Unityの非同期処理の比較

Unityの非同期処理の比較

中断処理が面倒なUniTask、Coroutineとの比較。
Unityの非同期処理について。

関連ページ
Unityで非同期処理を実装するには、コルーチン、Task、UniTaskの3つの選択肢があります。
このうち、現在の環境でわざわざTaskを選択する必要はなくて、必然的にCoroutineかUniTaskを使うことになると思います。

今現在の自分の考えとしては、UniTaskとCoroutineどちらか一択ではなく、場面場面で併用でも良い気がしています。
UniTaskは返り値を返すことができるのがとても便利です。
最近はダイアログをUniTaskで非同期処理で生成して、その返り値でダイアログのインスタンスを取得することなどを業務で行っています。
一方で中断処理が面倒という欠点もあります。

反対にCoroutineは中断処理が直感的で分かり易く、GameObjectが非アクティブになると自動で停止してくれるも利点。
これもこれもで、Unity開発では未だに便利だと思っています。

シンプルなコードで実装の比較

最初に、全く同じシンプルな実装をCoroutineとUniTask、Taskで行い、3者の比較をしていきます。
その次に中断処理について解説します。
すでに使い方をある程度知っている人は飛ばして大丈夫です。

実装する内容は、右のボタンを押すと星が5秒かけて右端に移動し、右端に到達すると星が点滅します。
左のボタンを押すと星が3秒かけて左端に移動し、左端に到達すると星が点滅します。
星の移動中は、上のテキストに経過時間が表示されます。


補足として今回は、星の移動処理に関してのみCoroutineでもUniTaskでもTaskでもなく Tween を使っていきます。(便利なので)

Coroutineを使った非同期処理

CoroutineはUnityEngineのnamespaceの中に入っているので、特に事前準備は必要ありません。
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

/// <summary>

/// Coroutineを使ったアニメーション実装

/// </summary>

public class TestCoroutine : MonoBehaviour
{
    [SerializeField]
    private Button rightButton;
    [SerializeField]
    private Button leftButton;
    [SerializeField]
    private Text infoText;
    [SerializeField]
    private Transform star;

    private float rightSlideSec = 5f;
    private float leftSlideSec = 3f;
    private float rightEndPosX = 500;
    private float leftEndPosX = -500;
    private int flashCount = 3;
    private float flashInterval = 0.2f;

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録

        rightButton.onClick.AddListener(OnTapRightButton);
        leftButton.onClick.AddListener(OnTapLeftButton);
    }

    
/// <summary>

    
/// 右のボタンを押した時の処理

    
/// </summary>

    private void OnTapRightButton()
    {
        StartCoroutine(CommonSlideAction(rightEndPosX, rightSlideSec));
    }

    
/// <summary>

    
/// 左のボタンを押した時の処理

    
/// </summary>

    private void OnTapLeftButton()
    {
        StartCoroutine(CommonSlideAction(leftEndPosX, leftSlideSec));
    }

    
/// <summary>

    
/// アニメーションの共通処理

    
/// </summary>

    private IEnumerator CommonSlideAction(float endPosX, float duration)
    {
        infoText.text = "経過時間0秒";

        
//星をTweenを使って移動させ、1秒置きにテキストを更新する

        star.DOLocalMoveX(endPosX, duration).SetEase(Ease.Linear);
        var remainTime = duration;
        while (remainTime > 0)
        {
            
//1秒待機

            yield return new WaitForSeconds(1);
            remainTime--;
            infoText.text = "経過時間" + (duration - remainTime) + "秒";
        }

        
//星が目的地に着いたら点滅させる

        for(int i = 0; i < flashCount * 2; i++)
        {
            
//偶数かどうか判定

            var isEven = i % 2 == 0;
            
//偶数なら星を非表示、奇数なら表示

            star.gameObject.SetActive(isEven ? false : true);
            
//0.2秒秒待機

            yield return new WaitForSeconds(flashInterval);
        }

        infoText.text = "やりましたね!!";
    }
}
返り値
 IEnumerator 
で定義された関数を、
 StartCoroutine 
関数で読んで非同期処理を行っています。
実際の待機処理のコードは
 yield return new WaitForSeconds 
の部分。

UniTaskを使った非同期処理

まずCysharpのGitHubから、対象のリポジトリをUnityPackageに追加する必要があります。
追加方法はこちら

スクリプト上でUniTaskを使うには
 using Cysharp.Threading.Tasks; 
を追記します。
using System;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;

/// <summary>

/// UniTaskを使ったアニメーション実装

/// </summary>

public class TestUniTask : MonoBehaviour
{
    [SerializeField]
    private Button rightButton;
    [SerializeField]
    private Button leftButton;
    [SerializeField]
    private Text infoText;
    [SerializeField]
    private Transform star;

    private float rightSlideSec = 5f;
    private float leftSlideSec = 3f;
    private float rightEndPosX = 500;
    private float leftEndPosX = -500;
    private int flashCount = 3;
    private float flashInterval = 0.2f;

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録

        rightButton.onClick.AddListener(OnTapRightButton);
        leftButton.onClick.AddListener(OnTapLeftButton);
    }

    
/// <summary>

    
/// 右のボタンを押した時の処理

    
/// </summary>

    private void OnTapRightButton()
    {
        
//CommonSlideActionの非同期処理を実行後、このOnTapRightButton関数は何もすることが無いのでForgetで待機を破棄する。

        CommonSlideAction(rightEndPosX, rightSlideSec).Forget();
    }

    
/// <summary>

    
/// 左のボタンを押した時の処理

    
/// </summary>

    private void OnTapLeftButton()
    {
        
//CommonSlideActionの非同期処理を実行後、このOnTapLeftButton関数は何もすることが無いのでForgetで待機を破棄する。        

        CommonSlideAction(leftEndPosX, leftSlideSec).Forget();
    }

    
/// <summary>

    
/// アニメーションの共通処理

    
/// </summary>

    private async UniTask CommonSlideAction(float endPosX, float duration)
    {
        infoText.text = "経過時間0秒";

        
//星をTweenを使って移動させ、1秒置きにテキストを更新する

        star.DOLocalMoveX(endPosX, duration).SetEase(Ease.Linear);
        var remainTime = duration;
        while (remainTime > 0)
        {
            
//UniTask.Delayは基本はミリ秒を渡す設計だが(1000ミリ秒=1秒)、TimeSpan.FromSecondを渡すことで秒数指定できる

            await UniTask.Delay(TimeSpan.FromSeconds(1));
            remainTime--;
            infoText.text = "経過時間" + (duration - remainTime) + "秒";
        }

        
//星が目的地に着いたら点滅させる

        for(int i = 0; i < flashCount * 2; i++)
        {
            
//偶数かどうか判定

            var isEven = i % 2 == 0;
            
//偶数なら星を非表示、奇数なら表示

            star.gameObject.SetActive(isEven ? false : true);
            
//TimeSpan.FromSecondに0.2を渡しているので0.2秒待機

            await UniTask.Delay(TimeSpan.FromSeconds(flashInterval));
        }

        infoText.text = "やりましたね!!";
    }
}
Coroutineの
 yeild return new WaitForSeconds 
に相当するのが
 await UniTask.Delay 

UniTask.Delayはミリ秒指定が基本なので、1秒待ちたい場合はTimeSpan.FromSecondsを間に挟みます。

関数を宣言するとき、Coroutineは返り値に
 IEnumerator 
を指定しますが、UniTaskは
 async UniTask 
と入力します。
Coroutineと違いその関数を呼ぶ時にStartCoroutineと入力する必要がなく、通常の関数と呼び出し方法が変わりません。
IEnumeratorの関数内ではyield return ~の待機処理コードが無いとエラーが出ますが、aync Taskでは待機処理コードが無くても別に問題ないです。

またUniTaskで特徴的なのがOnTapRightButton()とOnTapLeftButton()内で使っているForget()。
これは非同期処理を呼び出す側が、その非同期処理が終わるのを待機する必要がないことを表します。
仮にこのForgetを記入しない場合、C#は必要のないWarningを表示する。
Forgetを記入しない時のWarning表示
Warningが表示されるだけで実装上は何も問題がないですが、紛らわしいので待つ必要がないならForgetを付けて警告を消すべきです。

以上の細かい違いがありますが、このレベルの実装では正直CorouineでもUniTaskでもどっちでもいいです。

Taskを使った非同期処理

UniTaskはTaskの上位互換なので、Unityを使ってるならわざわざTaskを使う必要はないです。
でもUnity以外ではもしかして使う可能性もあるので自分用にメモ書き。

Taskを使用するにはusing System.Threading.Tasks;のコードを追加する必要があります。
using System;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using System.Threading.Tasks;

/// <summary>

/// Taskを使ったアニメーション実装

/// </summary>

public class TestTask : MonoBehaviour
{
    [SerializeField]
    private Button rightButton;
    [SerializeField]
    private Button leftButton;
    [SerializeField]
    private Text infoText;
    [SerializeField]
    private Transform star;

    private float rightSlideSec = 5f;
    private float leftSlideSec = 3f;
    private float rightEndPosX = 500;
    private float leftEndPosX = -500;
    private int flashCount = 3;
    private float flashInterval = 0.2f;

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録

        rightButton.onClick.AddListener(OnTapRightButton);
        leftButton.onClick.AddListener(OnTapLeftButton);
    }

    
/// <summary>

    
/// 右のボタンを押した時の処理

    
/// </summary>

    private void OnTapRightButton()
    {
        CommonSlideAction(rightEndPosX, rightSlideSec);
    }

    
/// <summary>

    
/// 左のボタンを押した時の処理

    
/// </summary>

    private void OnTapLeftButton()
    {
        CommonSlideAction(leftEndPosX, leftSlideSec);
    }

    
/// <summary>

    
/// アニメーションの共通処理

    
/// </summary>

    private async Task CommonSlideAction(float endPosX, float duration)
    {
        infoText.text = "経過時間0秒";

        
//星をTweenを使って移動させ、1秒置きにテキストを更新する

        star.DOLocalMoveX(endPosX, duration).SetEase(Ease.Linear);
        var remainTime = duration;
        while (remainTime > 0)
        {
            
//Task.Delayは基本はミリ秒を渡す設計だが(1000ミリ秒=1秒)、TimeSpan.FromSecondを渡すことで秒数指定できる

            await Task.Delay(TimeSpan.FromSeconds(1));
            remainTime--;
            infoText.text = "経過時間" + (duration - remainTime) + "秒";
        }

        
//星が目的地に着いたら点滅させる

        for(int i = 0; i < flashCount * 2; i++)
        {
            
//偶数かどうか判定

            var isEven = i % 2 == 0;
            
//偶数なら星を非表示、奇数なら表示

            star.gameObject.SetActive(isEven ? false : true);
            
//TimeSpan.FromSecondに0.2を渡しているので0.2秒待機

            await Task.Delay(TimeSpan.FromSeconds(flashInterval));
        }

        infoText.text = "やりましたね!!";
    }
}
TaskはUniTaskと違いForget()が用意されていない事です。
なので警告を消すことができません。地味にやっかい。

中断処理で実装の比較

コードに重大な違いが出てくるのが中断処理を実行する時。

さて、もし右のボタンを押した後、その処理が完了する前に左のボタンを押すと下の動画のようになります。

右にスライドする処理と左にスライドする処理が同時に走っています。
しかも右スライドの方がアニメーション時間が長いので、左のボタンを右ボタンの後に押しても最終的な星の座標は右端になっています。

こうした不自然で予期できない挙動を阻止するため、ゲーム実装では頻繁に中断処理を差し込むことになります。
修正した結果は次の通り。

Coroutineの中断処理

using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

/// <summary>

/// Coroutineを使ったアニメーション実装

/// </summary>

public class TestCoroutine : MonoBehaviour
{
    [SerializeField]
    private Button rightButton;
    [SerializeField]
    private Button leftButton;
    [SerializeField]
    private Text infoText;
    [SerializeField]
    private Transform star;

    private float rightSlideSec = 5f;
    private float leftSlideSec = 3f;
    private float rightEndPosX = 500;
    private float leftEndPosX = -500;
    private int flashCount = 3;
    private float flashInterval = 0.2f;
    
    
//中断処理のための変数を追加

    private Coroutine slideCoroutine;
    private Tweener slideTweener;

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録        

        rightButton.onClick.AddListener(OnTapRightButton);
        leftButton.onClick.AddListener(OnTapLeftButton);
    }
 
     
/// <summary>

    
/// 右のボタンを押した時の処理

    
/// </summary>

    private void OnTapRightButton()
    {
        
//実行中のコルーチンがある場合中断        

        if (slideCoroutine != null)
        {
            StopCoroutine(slideCoroutine);
            slideTweener.Kill();
        }
        slideCoroutine = StartCoroutine(CommonSlideAction(rightEndPosX, rightSlideSec));
    }

    
/// <summary>

    
/// 左のボタンを押した時の処理

    
/// </summary>

    private void OnTapLeftButton()
    {
        
//実行中のコルーチンがある場合中断        

        if (slideCoroutine != null)
        {
            StopCoroutine(slideCoroutine);
            slideTweener.Kill();
        }
        slideCoroutine = StartCoroutine(CommonSlideAction(leftEndPosX, leftSlideSec));
    }

     
/// <summary>

    
/// アニメーションの共通処理

    
/// </summary>

    private IEnumerator CommonSlideAction(float endPosX, float duration)
    {
        star.gameObject.SetActive(true);

        infoText.text = "経過時間0秒";

        
//星をTweenを使って移動させ、1秒置きにテキストを更新する

        
//中断処理のため実行したTweenは変数に保存

        slideTweener = star.DOLocalMoveX(endPosX, duration).SetEase(Ease.Linear);
        var remainTime = duration;
        while (remainTime > 0)
        {
            
//1秒待機

            yield return new WaitForSeconds(1);
            remainTime--;
            infoText.text = "経過時間" + (duration - remainTime) + "秒";
        }

        
//星が目的地に着いたら点滅させる

        for (int i = 0; i < flashCount * 2; i++)
        {
            
//偶数かどうか判定            

            var isEven = i % 2 == 0;
            
//偶数なら星を非表示、奇数なら表示            

            star.gameObject.SetActive(isEven ? false : true);
            
//0.2秒秒待機            

            yield return new WaitForSeconds(flashInterval);
        }

        infoText.text = "やりましたね!!";
    }
}
 private Coroutine slideCoroutine; 
この変数に実行するCoroutineをあらかじめ保存します。
次にボタンを押す際にはまず実行中のコルーチンを停止させます。

また星の移動にはTweenを使っているますが、TweenはCoroutineとは関係ない独立したスレッドで動いています。
なので
 private Tweener slideTweener; 
で実行するTweenを保存させています。Coroutineの停止と同じタイミングで停止を呼びます。

UniTaskの中断処理

UniTaskで停止処理を行うには、
 using System.Threading; 
を新たに追記します。
using System;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;

/// <summary>

/// UniTaskを使ったアニメーション実装

/// </summary>

public class TestUniTask : MonoBehaviour
{
    [SerializeField]
    private Button rightButton;
    [SerializeField]
    private Button leftButton;
    [SerializeField]
    private Text infoText;
    [SerializeField]
    private Transform star;

    private float rightSlideSec = 5f;
    private float leftSlideSec = 3f;
    private float rightEndPosX = 500;
    private float leftEndPosX = -500;
    private int flashCount = 3;
    private float flashInterval = 0.2f;

    
//中断処理のための変数を追加

    private CancellationTokenSource slideTokenSource;
    private Tweener slideTweener;

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録

        rightButton.onClick.AddListener(OnTapRightButton);
        leftButton.onClick.AddListener(OnTapLeftButton);
    }

    
/// <summary>

    
/// 右のボタンを押した時の処理

    
/// </summary>

    private void OnTapRightButton()
    {
        
//実行中のタスクがある場合中断

        if (slideTokenSource != null)
        {
            slideTokenSource.Cancel();
            slideTweener.Kill();
        }
        slideTokenSource = new CancellationTokenSource();
        CommonSlideAction(rightEndPosX, rightSlideSec, slideTokenSource.Token).Forget();
    }

    
/// <summary>

    
/// 左のボタンを押した時の処理

    
/// </summary>

    private void OnTapLeftButton()
    {
        
//実行中のタスクがある場合中断

        if (slideTokenSource != null)
        {
            slideTokenSource.Cancel();
            slideTweener.Kill();
        }
        slideTokenSource = new CancellationTokenSource();
        CommonSlideAction(leftEndPosX, leftSlideSec, slideTokenSource.Token).Forget();
    }

    
/// <summary>

    
/// アニメーションの共通処理

    
/// </summary>

    private async UniTask CommonSlideAction(float endPosX, float duration, CancellationToken token)
    {
        
//中断を実行したタイミングによっては後半の点滅で

        
//star.gameOjbectが消えてる可能性があるのでtrueにする

        star.gameObject.SetActive(true);

        infoText.text = "経過時間0秒";

        
//星をTweenを使って移動させ、1秒置きにテキストを更新する

        
//中断処理のため実行したTweenは変数に保存

        slideTweener = star.DOLocalMoveX(endPosX, duration).SetEase(Ease.Linear);
        var remainTime = duration;
        while (remainTime > 0)
        {
            
//UniTask.Delayは基本はミリ秒を渡す設計だが(1000ミリ秒=1秒)、TimeSpan.FromSecondを渡すことで秒数指定できる

            await UniTask.Delay(TimeSpan.FromSeconds(1));
            
//タスクの中断の命令が出ていればreturn

            if(token.IsCancellationRequested)
            {
                return;
            }
            remainTime--;
            infoText.text = "経過時間" + (duration - remainTime) + "秒";
        }

        
//星が目的地に着いたら点滅させる

        for(int i = 0; i < flashCount * 2; i++)
        {
            
//偶数かどうか判定

            var isEven = i % 2 == 0;
            
//偶数なら星を非表示、奇数なら表示

            star.gameObject.SetActive(isEven ? false : true);
            
//TimeSpan.FromSecondに0.2を渡しているので0.2秒待機

            await UniTask.Delay(TimeSpan.FromSeconds(flashInterval));
            
//タスクの中断の命令が出ていればreturn

            if(token.IsCancellationRequested)
            {
                return;
            }
        }

        infoText.text = "やりましたね!!";
    }
}
UniTaskの中断処理では
 StopCoroutine 
ではなく
 CancellationTokenSource 
 CancellationToken 
を使います。
これが直感的でなく非常に分かりずらいです。
中断命令はCancellationTokenSourceで行いますが、中断命令が出されたかどうかのフラグはCancellationTokenを見る必要があります。

UniTaskの中断処理は、StopCoroutineのようにCoroutine関数の外部で行うのではなく、Task関数の内部で行う必要があります。
下が実際の中断処理のコード。
            if(token.IsCancellationRequested)
            {
                return;
            }
しかもUniTask関数の内部の全てのawait UniTask.Delayの後ろに中断処理を書き込む必要があります。
これが厄介で、長いアニメーションでは頻繁にUniTask.Delayを行うので、無意味に冗長で汚いコードになります。抜けも起きやすいです。

UniTaskに関する不満はこれ一点で、ここさえ解決してくれれば良いのにとずっと思ってる。

Taskの中断処理

Taskの中断処理もUniTaskと同じ仕様。
using System;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using System.Threading.Tasks;
using System.Threading;

/// <summary>

/// Taskを使ったアニメーション実装

/// </summary>

public class TestTask : MonoBehaviour
{
    [SerializeField]
    private Button rightButton;
    [SerializeField]
    private Button leftButton;
    [SerializeField]
    private Text infoText;
    [SerializeField]
    private Transform star;

    private float rightSlideSec = 5f;
    private float leftSlideSec = 3f;
    private float rightEndPosX = 500;
    private float leftEndPosX = -500;
    private int flashCount = 3;
    private float flashInterval = 0.2f;

    
//中断処理のための変数を追加

    private CancellationTokenSource slideTokenSource;
    private Tweener slideTweener;

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録

        rightButton.onClick.AddListener(OnTapRightButton);
        leftButton.onClick.AddListener(OnTapLeftButton);
    }

    
/// <summary>

    
/// 右のボタンを押した時の処理

    
/// </summary>

    private void OnTapRightButton()
    {
        
//実行中のタスクがある場合中断

        if (slideTokenSource != null)
        {
            slideTokenSource.Cancel();
            slideTweener.Kill();
        }
        slideTokenSource = new CancellationTokenSource();
        CommonSlideAction(rightEndPosX, rightSlideSec, slideTokenSource.Token);
    }

    
/// <summary>

    
/// 左のボタンを押した時の処理

    
/// </summary>

    private void OnTapLeftButton()
    {
        
//実行中のタスクがある場合中断

        if (slideTokenSource != null)
        {
            slideTokenSource.Cancel();
            slideTweener.Kill();
        }
        slideTokenSource = new CancellationTokenSource();
        CommonSlideAction(leftEndPosX, leftSlideSec, slideTokenSource.Token);
    }

    
/// <summary>

    
/// アニメーションの共通処理

    
/// </summary>

    private async Task CommonSlideAction(float endPosX, float duration, CancellationToken token)
    {
        
//中断を実行したタイミングによっては後半の点滅で

        
//star.gameOjbectが消えてる可能性があるのでtrueにする

        star.gameObject.SetActive(true);

        infoText.text = "経過時間0秒";

        
//星をTweenを使って移動させ、1秒置きにテキストを更新する

        
//中断処理のため実行したTweenは変数に保存

        slideTweener = star.DOLocalMoveX(endPosX, duration).SetEase(Ease.Linear);
        var remainTime = duration;
        while (remainTime > 0)
        {
            
//Task.Delayは基本はミリ秒を渡す設計だが(1000ミリ秒=1秒)、TimeSpan.FromSecondを渡すことで秒数指定できる

            await Task.Delay(TimeSpan.FromSeconds(1));
            
//タスクの中断の命令が出ていればreturn

            if(token.IsCancellationRequested)
            {
                return;
            }
            remainTime--;
            infoText.text = "経過時間" + (duration - remainTime) + "秒";
        }

        
//星が目的地に着いたら点滅させる

        for(int i = 0; i < flashCount * 2; i++)
        {
            
//偶数かどうか判定

            var isEven = i % 2 == 0;
            
//偶数なら星を非表示、奇数なら表示

            star.gameObject.SetActive(isEven ? false : true);
            
//TimeSpan.FromSecondに0.2を渡しているので0.2秒待機

            await Task.Delay(TimeSpan.FromSeconds(flashInterval));
            
//タスクの中断の命令が出ていればreturn

            if(token.IsCancellationRequested)
            {
                return;
            }
        }

        infoText.text = "やりましたね!!";
    }
}

UniTaskの返り値について

UniTask(もしくはTask)で一番自分が便利だと思うのは、Genericで返り値を指定できることです。
これはCoroutineでは真似できないです。

試しに下の動画のような処理を作ってみます。
「計測スタート」のボタンをタップした後、右が左のキーを計5回タップすると、押した回数が多い方に星が移動します。


コードは下のような感じ。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Linq;

public class TestUniTaskMeasure : MonoBehaviour
{
    [SerializeField]
    private Button startMeasureButton;
    [SerializeField]
    private Text keyDownStatusText;
    [SerializeField]
    private Transform star;    

    
//keyDownRecordにAddする文字列を事前にreadonlyで定義

    private readonly string leftString = "Left";
    private readonly string rightString = "Right";

    
//計測終了までに必要なキーの押下回数

    private readonly int needKeyDownCount = 5;
    private int curKeyDownCount = 0;

    
//計測結果に応じて移動させる星の最終座標

    private float leftEndPosX = -500;    
    private float rightEndPosX = 500;

    
//どちらのキーを押したかの記録を保存するためのList

    private List<string> keyDownRecord = new List<string>();

    private void Start()
    {
        
//ボタンイベントをスクリプトから登録

        startMeasureButton.onClick.AddListener(() => OnTapStartMeasure().Forget());   
    }

    
/// <summary>

    
/// 計測スタートのボタンを押した時の処理

    
/// </summary>

    private async UniTask OnTapStartMeasure()
    {
        
//ボタン表示を一時的に消す

        startMeasureButton.gameObject.SetActive(false);
        keyDownStatusText.text = "計測中...";

        
//初期化処理

        keyDownRecord.Clear();
        curKeyDownCount = 0;
        Vector2 curPos = star.localPosition;
        star.localPosition = new Vector3(0, curPos.y);
        
        
//Measure関数の終了をawaitで待機し、結果はresultに代入

        string resultString = await Measure();

        
//結果が左ならleftEndPosXを、右ならrightEndPosをresultPosに代入

        float resultPos = resultString == leftString ? leftEndPosX : rightEndPosX;
        
//星のx座標を移動させる

        star.localPosition = new Vector3(resultPos, curPos.y);

        
//ボタン表示を復活させる

        startMeasureButton.gameObject.SetActive(false); 
        keyDownStatusText.text = "計測終了! 結果 = " + resultString;
    }

    
/// <summary>

    
/// キーダウンの計測を開始。右矢印、もしくは左矢印を計5回押すまで計測を続ける

    
/// 計測が終わったら、どちらの矢印をより多く押したかの結果を返す

    
/// </summary>

    private async UniTask<string> Measure()
    {
        while(true)
        {
            
//キーボードの左矢印を押下

            if(Input.GetKeyDown(KeyCode.LeftArrow))
            {
                keyDownRecord.Add(leftString);
                curKeyDownCount++;                
                keyDownStatusText.text = leftString + "のキーを押下! 押下回数" + curKeyDownCount;
            }
            
//キーボードの右矢印を押下

            if(Input.GetKeyDown(KeyCode.RightArrow))
            {
                keyDownRecord.Add(rightString);
                curKeyDownCount++;                
                keyDownStatusText.text = rightString + "のキーを押下! 押下回数" + curKeyDownCount;
            }

            
//キーの押下回数がneedKeyDownCountに達したら計測終了処理へ

            if(curKeyDownCount >= needKeyDownCount)
            {
                
//LINQでkeyDownRecord内の左矢印を押した回数と右矢印を押した回数を取得

                int leftCount = keyDownRecord.Count(x => x == leftString);
                int rightCount = keyDownRecord.Count(x => x == rightString);

                
//leftCountの方が大きければleftStringを、そうでなければrightStringを結果として返す

                return leftCount > rightCount? leftString : rightString;
            }

            
//1フレームだけ待機

            await UniTask.Yield();
        }
    }
}
OnTapStartMeasure()関数の中で、
        
//Measure関数の終了をawaitで待機し、結果はresultに代入

        string resultString = await Measure();
といった具合にMeasure()関数の終了をawaitで待っています。

Measure()関数の定義を見てみると、返り値がUniTask<string>と指定してあります。
    private async UniTask<string> Measure()
つまりこの関数は、返り値に必ずstringを返す必要があるUniTaskの非同期処理関数ということになります。

Measure()関数の後半に実際にその返り値を返す処理が打ち込んである。
            
//キーの押下回数がneedKeyDownCountに達したら計測終了処理へ

            if(curKeyDownCount >= needKeyDownCount)
            {
                
//LINQでkeyDownRecord内の左矢印を押した回数と右矢印を押した回数を取得

                int leftCount = keyDownRecord.Count(x => x == leftString);
                int rightCount = keyDownRecord.Count(x => x == rightString);

                
//leftCountの方が大きければleftStringを、そうでなければrightStringを結果として返す

                return leftCount > rightCount? leftString : rightString;
            }

今回たまたまGenericでstringを指定していますが、別にboolでも、intでも、自分が定義したクラスでも、なんでも返すことが出来ます。
0
0