MRが楽しい

MRやVRについて学習したことを書き残す

UniTaskを使って徐々に変化するゲージバーを作成する その2(連続で呼び出された場合は前の処理を省略する)

本日はUniTaskの小ネタ枠です。
UniTaskを使って徐々に変化するゲージバーを作成する方法を記事に残します。
今回は少し応用して連続で呼び出された場合は前の処理を省略するコードを作成します。

前回記事

以下の前回記事の続きです。
bluebirdofoz.hatenablog.com

UniTask.YieldのcancellationToken引数

UniTask.YieldのcancellationToken引数にCancellationTokenSourceを引き渡すことで任意のタイミングでOperationCanceledExceptionを発生させることができます。

// キャンセルトークンを作成
var _cancelTokenSource = new CancellationTokenSource();

try
{
    while (true)
    {
        // フレームごとの実行処理
                
        // キャンセル(_cancelTokenSource?.Cancel())が要求された場合にOperationCanceledExceptionを発生させて即座に処理を終了する
        await UniTask.Yield(cancellationToken: _cancelTokenSource.Token);
    }
}
catch (OperationCanceledException)
{
    // キャンセルされた場合に実行する処理
}
finally
{
    // 最後に必ず実行する処理
}

サンプルスクリプト
前回作成したスクリプトを改修して連続して増加関数を呼び出すと、ひとつ前のアニメーションを省略して次のアニメーションに即座に移行するようにしました。
・SmoothStretchGaugeTest2.cs

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

public class SmoothStretchGaugeTest2 : MonoBehaviour
{
    /// <summary>
    /// 全体のサイズ
    /// </summary>
    [SerializeField] private RectTransform _totalRectTransform;
    
    /// <summary>
    /// 青ゲージ
    /// </summary>
    [SerializeField] private RectTransform _blueRectTransform;

    /// <summary>
    /// 赤ゲージ
    /// </summary>
    [SerializeField] private RectTransform _redRectTransform;

    /// <summary>
    /// 現在の青ゲージの比率
    /// </summary>
    [SerializeField] private float _currentBlueRatio = 0.0f;

    /// <summary>
    /// アニメーションの排他制御
    /// </summary>
    private SemaphoreSlim _semaphoreSlim = new (1,1);

    /// <summary>
    /// キャンセル用トークン
    /// </summary>
    private CancellationTokenSource _cancelTokenSource;
    
    async void Start()
    {
        ChangeGauge(_currentBlueRatio);
        
        // 2秒後にゲージを増加させる
        await UniTask.Delay(TimeSpan.FromSeconds(2));
        ChangeGaugeAnimationAsync(30).Forget();
        
        // 2秒後にゲージを増加させる
        await UniTask.Delay(TimeSpan.FromSeconds(2));
        ChangeGaugeAnimationAsync(30).Forget();
    }

    public async UniTask ChangeGaugeAnimationAsync(
        int incrementRatio,
        float tweenTime = 5.0f)
    {
        float animationTime = 0;

        try
        {
            // ひとつ前の処理で実行中のキャンセルトークンが存在する場合
            // 即座にキャンセルを呼び出してひとつ前の処理を終了する
            _cancelTokenSource?.Cancel();

            // アニメーション中は排他制御を行い、アニメーションが終わるまで必ず待機する
            await _semaphoreSlim.WaitAsync();
            
            // 本スレッド用にキャンセルトークンを作成
            _cancelTokenSource = new CancellationTokenSource();
            
            // アニメーションの開始地点と終了地点を計算する
            var animationStartRatio = _currentBlueRatio;
            var animationEndRatio = _currentBlueRatio + incrementRatio;

            // キャンセルに備えて現在値は先に更新しておく
            _currentBlueRatio = animationEndRatio;

            while (animationTime < tweenTime)
            {
                // ゲージを徐々に変化させる
                animationTime += Time.deltaTime;
                var progressRatio = (int)Mathf.Lerp(animationStartRatio, animationEndRatio,
                    animationTime / tweenTime);
                ChangeGauge(progressRatio);
                
                // キャンセルが要求された場合は即座に処理を終了する
                await UniTask.Yield(cancellationToken: _cancelTokenSource.Token);
            }

            ChangeGauge(animationEndRatio);
        }
        catch (OperationCanceledException)
        {
            // キャンセルされた場合は最終値に更新する
            ChangeGauge(_currentBlueRatio);
        }
        finally
        {
            // 本スレッドのキャンセルトークンを解放する
            _cancelTokenSource = null;
            
            // アニメーションが終わったら排他制御を解除する
            _semaphoreSlim.Release();
        }
    }

    private void ChangeGauge(float blueRatio)
    {
        var totalGaugeWidth = _totalRectTransform.rect.width;
        var blueGaugeWidth = totalGaugeWidth * blueRatio / 100.0f;
        var redGaugeWidth = totalGaugeWidth - blueGaugeWidth;
        
        _blueRectTransform.offsetMin = new Vector2(0, 0);
        _blueRectTransform.offsetMax = new Vector2(-1.0f * redGaugeWidth, 0);

        _redRectTransform.offsetMin = new Vector2(blueGaugeWidth, 0);
        _redRectTransform.offsetMax = new Vector2(0, 0);
    }
}

シーンを再生すると自動で2秒間隔でゲージの増加関数が呼びされます。

2秒経過するとゲージの増加が始まりますが、その2秒後、即座に現在のアニメーションが省略されて次のアニメーションに移行します。