💬

[Unity][R3]ゲームオーバー処理が複数呼ばれることを防ぐ

2024/02/25に公開

はじめに

Unityでゲームオーバー処理が複数回呼ばれてしまい戸惑った経験はありませんか?そんな問題は、R3のThrottleLastオペレーターを使うことで解決できます。

R3とは

R3については下記の記事を引用してみます。

「R3」はReactiveExtensions(Rx)を現代に合わせてより洗練した形に再定義/再実装した、C#用のライブラリです。
https://qiita.com/toRisouP/items/e7be5a5a43058556db8f

ThrottleLastとは

ThrottleLastは、メッセージが到達したら非同期処理を開始します。非同期処理中はメッセージが来ても遮断します。非同期が終わった時に届いていた最後のメッセージを発行する処理です。
なので、ゲームオーバー処理で言えば、ゲームオーバー演出の非同期処理中にダメージ処理が何度も入り、ゲームオーバー処理が複数回行われることを防ぐことができます。

実装

実際にゲームオーバー処理処理を何度も呼ばれることを防ぐ処理を下記に示します。

Player.cs
using System.Threading;
using Cysharp.Threading.Tasks;
using R3;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class Player : MonoBehaviour
{
    private readonly ReactiveProperty<int> currentHealth = new(5);
    [SerializeField] private Button button;
    [SerializeField] private Text healthText;
    [SerializeField] private Slider processSlider;
    private void Start()
    {
        // ボタンを押下するとダメージ処理を行う
        button.OnClickAsObservable()
            .Subscribe(_=>TakeDamage(1));

        currentHealth.
            SubscribeToText(healthText);
        
        // ダメージが0以下の時に死亡処理を呼び出す
        currentHealth
            .Where(health=>health<=0)
            .ThrottleLast((_, ct) => GameOverStaging(ct))
            .Subscribe(_=>LoadSceneToGameOver())
            .AddTo(this);
    }

    /// <summary>
    /// ゲームオーバー演出
    /// </summary>
    private async UniTask GameOverStaging(CancellationToken ct)
    {
        processSlider.value = 0;

        var currentTime = 0f;
        var waitSecond=5;

        while (!ct.IsCancellationRequested && currentTime < waitSecond)
        {
            await UniTask.Yield();
            currentTime += Time.deltaTime;
            processSlider.value = Mathf.Clamp01(currentTime / waitSecond);
        }
    }

    /// <summary>
    /// ゲームオーバーシーンへの遷移
    /// </summary>
    private void LoadSceneToGameOver()
    {
        SceneManager.LoadScene("GameOver");
    }

    /// <summary>
    /// ダメージ処理
    /// </summary>
    private void TakeDamage(int damage)
    {
        currentHealth.Value-=damage;
        Debug.Log("現在のHP"+currentHealth.Value);
    }
}

処理の解説

ダメージ処理

この処理ではUIボタンを押下した際に、ダメージ処理を発生させています。

button.OnClickAsObservable()
            .Subscribe(_=>TakeDamage(1));

/// <summary>
/// ダメージ処理
/// </summary>
private void TakeDamage(int damage)
{
    currentHealth.Value-=damage;
    Debug.Log("現在のHP"+currentHealth.Value);
}

ゲームオーバー処理

体力が変化した時、且つ、体力が0以下の時にダメージ演出「GameOverStaging」を呼び出します。その間、体力が変化してるときに発生するメッセージを遮断します。そして最後に届いていたメッセージを元に、「LoadSceneToGameOver」を行います。ポイントとして、非同期中メッセージを遮断しているのが重要です。この間に、何度体力が変化しようと無視してくれます。

// ダメージが0以下の時に死亡処理を呼び出す
currentHealth
    .Where(health=>health<=0)
    .ThrottleLast((_, ct) => GameOverStaging(ct))
    .Subscribe(_=>LoadSceneToGameOver())
    .AddTo(this);

/// <summary>
/// ゲームオーバー演出
/// </summary>
private async UniTask GameOverStaging(CancellationToken ct)
{
    processSlider.value = 0;
    var currentTime = 0f;
    var waitSecond=5;
    while (!ct.IsCancellationRequested && currentTime < waitSecond)
    {
        await UniTask.Yield();
        currentTime += Time.deltaTime;
        processSlider.value = Mathf.Clamp01(currentTime / waitSecond);
    }
}

/// <summary>
/// ゲームオーバーシーンへの遷移
/// </summary>
private void LoadSceneToGameOver()
{
    SceneManager.LoadScene("GameOver");
}

実際の動作

実際の動作例にある様に、ダメージ処理が何度入ろうと、死亡演出が繰り返さないことを確認できました。たとえば死亡したキャラクターにも、ダメージ演出だけ通したい時などには今回の手法が有効です。
実際の動作

おわりに

筆者もR3を触って期間が浅いので、この手法が最適化分かりかねています。コメント欄などでこうした方が良いなどあれば、ご教授頂きたいです。もしこの記事が良いなと思った方は、いいねとフォローをお願いします。

Discussion