💭

[Unity]経験値バーなどに使える自動で遷移するゲージ

2024/01/19に公開

実装物

概要

経験値など何かしらのポイントを獲得した際に、その進捗状況を表すためのゲージを実装しました。

やりたかったこととしては以下。

  • ゲージが最大にまで達したら(超過したら)、最大で止まるのでなく0から始まってまた勝手に増加する。
  • ゲージの最大値は段階ごとに増加していく。
  • 遷移が終わったタイミングで何かしらの処理を行えるようにしたい。

自動で遷移するゲージはいろいろな方が実装されていますが、どれも一度最大になったらそこで終わりというものが多かったので自分で実装してみました。
せっかくなので記事にしてみます。
(あまり動作確認ができていないのでもし不具合とかあれば指摘いただけると幸いです。)

なお、この記事ではUniTaskを使用しています。

コード

ひとまず、ゲージを制御しているクラスのコードです。

MovableGauge.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using Cysharp.Threading.Tasks;
using System.Linq;

/*
 * 
 *  @ 自動で遷移するゲージの制御クラス
 *  
 */
public class MovableGauge : MonoBehaviour
{
    /*
     *  @ ゲージ遷移処理で使用する変数群
     */
    public class Args
    {
        //! 初期値
        public int startValue = 0;

        //! 変動値
        public int changeValue = 0;

        //! ゲージの最大値 (段階ごとに変動する可能性があるのでリスト)
        public List<int> fillValueList = new List<int>();

        //! 遷移に要する時間
        public float moveTime = 1f;
	
	//! UniTaskキャンセル用
        public CancellationToken cancellationToken;
	
	// コンストラクタ
        public Args(int startValue, int changeValue, List<int> fillValueList, float moveTime, CancellationToken cancellationToken)
        {
            this.startValue = startValue;
            this.changeValue = changeValue;
            this.fillValueList = fillValueList;
            this.moveTime = moveTime;
            this.cancellationToken = cancellationToken;
        }
    }

    //! スライダーコンポーネント
    [SerializeField] protected Slider _gauge = null;

    /*
     *  @ ゲージの値を直接指定する
     */
    public void SetGaugeValue( float value )
    {
        _gauge.value = value;
    }


    /*
     *  @ ゲージ遷移開始関数
     *  @ return : ゲージが動いたかどうか
     */
    public async UniTask<bool> MoveGauge( Args args )
    {
        // キャンセル確認
        args.cancellationToken.ThrowIfCancellationRequested();

        return await MoveGauge_Impl(args);
    }

    /*
     *  @ ゲージ遷移のメイン処理
     *  @ return : ゲージが動いたかどうか
     */
    private async UniTask<bool> MoveGauge_Impl( Args args )
    {
        // キャンセル確認
        args.cancellationToken.ThrowIfCancellationRequested();
	
	// 変動なし
        if (args.changeValue <= 0) return false;

        // 最大値
        int maxValue = args.fillValueList.Last();

        // すでに最大値なので終了
        if (args.startValue >= maxValue) return false;

        // 目標値
        // 最大値を超過しないよう丸める
        int targetValue =  Math.Clamp(( args.startValue + args.changeValue ), 0, maxValue );

        // 計測時間
        float elapsedTime = 0f;

        // ゲージ遷移が完了するまで繰り返し
        while (true)
        {
            // 経過時間加算
            elapsedTime += Time.deltaTime;

            // 要する時間と経過時間から進捗率を計算
            float rate = (elapsedTime / args.moveTime);

            // 現在値を計算
            float currentValue = Mathf.Lerp(args.startValue, targetValue, rate);

            // ゲージの最大値を調べる
            float gaugeFillValue = 0f;
            float subtractValue = 0f;
            foreach( float value in args.fillValueList )
            {
                if ( currentValue <= value)
                {
                    gaugeFillValue = value;
                    break;
                }

                subtractValue = value;
            }

            // ゲージ用の値に変換
            // スライダーの値は0.0~1.0
            float gaugeValue = (currentValue - subtractValue) / ( gaugeFillValue - subtractValue );

            // ゲージを動かす
            _gauge.value = gaugeValue;

            // ゲージ遷移中に行いたい処理
            OnGaugeUpdate(currentValue, gaugeFillValue);

            // 所要時間を経過したら終了
            if (elapsedTime >= args.moveTime)
            {
                break;
            }

            // 1フレーム待機
            await UniTask.DelayFrame(1, cancellationToken: args.cancellationToken);
        }

        // ゲージが最大の状態
        // かつ、まだ最大値には達していないので0に戻しておく
        if (_gauge.value >= 1.0f && targetValue < maxValue )
        {
            _gauge.value = 0f;
        }
	
	return true;
    }

    /*
     *  @ ゲージ遷移中に毎フレーム行いたい処理があれば継承先で記述する
     *  @ 引数はやりたいこと次第
     */
    protected virtual void OnGaugeUpdate(float currentValue, float fillGaugeValue)
    {
        // テキストの更新とか
        // エフェクトの再生とか?
    }
}

呼び出し側のコード。
動作確認のためのものでかなり適当なのでご了承ください。

UIController.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks.Linq;
using UniRx;

/*
 * 
 *  @ 呼び出し側
 *  
 */
public class UIController : MonoBehaviour
{
    //! ゲージ
    [SerializeField] protected MovableGauge _movableGauge = null;

    //! ゲージ動作ボタン
    [SerializeField] protected Button _gaugeMoveButton = null;

    //! 現在の所持ポイント
    protected int _currentPoint = 0;

    //! 獲得するポイント
    protected readonly int GetPoint = 12;

    // Start is called before the first frame update
    void Start()
    {
        // ボタンを押すとゲージが動き出す
        _gaugeMoveButton.OnClickAsAsyncEnumerable().SubscribeAwait(async _=> await StartGauge() ).AddTo(this);

        // 初期設定
        _movableGauge.SetGaugeValue(0f);
    }

    protected async UniTask StartGauge()
    {
        // ポイント獲得(適当)
        int tmpCurrentPoint = _currentPoint;
        _currentPoint += GetPoint;

        // ゲージを動かす
        bool isMoveGauge = await _movableGauge.MoveGauge(new MovableGauge.Args
        (
            startValue: tmpCurrentPoint,
            changeValue: GetPoint,
            fillValueList:  new List<int>()
            {
                10,30,60
            },
            moveTime: 1,
            cancellationToken: _movableGauge.GetCancellationTokenOnDestroy()
        ));

        // 完了時の処理
        // 演出とかダイアログ表示とかの完了処理
        if (isMoveGauge)
        {
        }
    }

}

使い方

下の画像のようにMovableGauge.csを任意のオブジェクトにアタッチし、Gaugeの欄にSliderコンポーネントを持つオブジェクトを設定。

あとはMoveGauge()を呼んでやればOK。
awaitすればそのあとの処理はゲージ遷移が完了してから行われる。
返り値は「ゲージ遷移が行われたかどうか」なので、ゲージが動いたときだけ処理を行うことも可能。

        // ゲージを動かす
        bool isMoveGauge = await _movableGauge.MoveGauge(new MovableGauge.Args
        (
            startValue: ,
            changeValue: ,
            fillValueList:  ,
            moveTime: ,
            cancellationToken: ,
        ));

指定するパラメータに関して説明。

  • startValue
    ゲージが変動する前の初期値。これまでの累計獲得ポイント数
    _currentValueが格納されており、この値はボタンを押すごとに更新されている。
  • changeValue
    変動量。
    今回はGetPointで定義されており、12で固定。
  • fillValueList
    ゲージが最大になる値のリスト。必要累計ポイント数
    今回は次に示すようなテーブルを想定しているので、10,30,60と格納されている。
  • moveTime
    変動に要する時間(秒数)。
    今回は1秒で設定。
  • cancellationToken
    UniTaskのキャンセル用。任意のもので。
レベル 必要累計ポイント (前レベルとの差)
1 10 (10)
2 30 (20)
3 60 (30)

おわりに

補足ですが、変数を累計にしている理由は「基本的にマスターやセーブデータは累計値で定義されるかな」とという認識があったからです。
拾った値をそのまま放り投げれた方が使う側が楽かなと思ってそうしているだけなので、プロジェクトなどによっては逆に不便になるかもしれませんね..。

それと、作ってから思いましたがSliderではなくてImageのfillAmountを使っても良かったかもと思ったり。

以上です。個人的にはそこそこ目的に沿うものは作れたかなと。
何か不具合やもっとこうした方が良いなどの指摘があればご教授いただけると幸いです。

Discussion