💭

[Unity]UniTaskを使ってスキップ機能を作った

2024/01/24に公開

はじめに

UniTaskの勉強がてら、ゲームで必要不可欠なスキップ機能の実装をしてみました。
作ったものは主にインタフェースとマネージャーの二つです。

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

制作物

インタフェース。
ゲージやカウントダウン、ムービーといった、スキップされる演出物に実装する。

ISkippable.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading;
using Cysharp.Threading.Tasks;

/*
 * 
 *  @ スキップ可能インタフェース
 *  
 */
public interface ISkippable<T>
    where T : class
{
    // メインの処理の開始
    public abstract UniTask OnStart(T args, CancellationToken cancellationToken);
    
    // スキップされたときの処理
    public abstract UniTask OnSkip(T args, CancellationToken cancellationToken);
}

マネージャー。
スキップ処理の本体。
必要なパラメータとISkippableを渡すことで、もろもろの処理を勝手に行ってくれる。

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

/*
 * 
 *  @ スキップ処理の制御クラス
 *  
 */
public class SkipManager<T>
    where T : class
{
    public class Parameter
    {
        //! 実行したい演出
        public ISkippable<T> skippableObj = null;

        //! OnStartとOnSkipに渡したいクラス
        //  ジェネリックで指定したもの
        public T args = null;

        //! スキップのトリガーとなるイベント
        //  ボタン押下など
        public IObservable<Unit> skipTrigger = null;

        //! 任意のCancellationToken
        //  基本的にはISkippableを実装しているクラスのGetCancellationTokenOnDestroy()
        public CancellationToken parentCt;
        
        // コンストラクタ
        public Parameter(ISkippable<T> skippableObj, T args, IObservable<Unit> skipTrigger, CancellationToken parentCt)
        {
            this.skippableObj = skippableObj;
            this.args = args;
            this.skipTrigger = skipTrigger;
            this.parentCt = parentCt;
        }
    }

    //! パラメータ
    protected Parameter _parameter = null;

    //! スキップ用のCancellationToken
    protected CancellationTokenSource _skipCts;

    //! Disposal
    protected IDisposable _disposable = null;

    /*
     *  @ コンストラクタ
     */
    public SkipManager(Parameter parameter)
    {
        this._parameter = parameter;

        _skipCts = new CancellationTokenSource();

        // イベントの登録
        // スキップのトリガーが飛んで来たらキャンセルする
        _disposable = _parameter.skipTrigger.Subscribe( _ => _skipCts.Cancel() );
    }

    /*
     *  @ 実行処理
     */
    public async UniTask Process()
    {
        // キャンセル確認
        _parameter.parentCt.ThrowIfCancellationRequested();

        // CancellationTokenを合成
        // 引数で指定したCancellationTokenのどちらかがキャンセルされたら、キャンセルが飛ぶ
        CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_skipCts.Token, _parameter.parentCt);

        // 演出の実行
        // キャンセルされたらTrueが帰ってくる
        bool isSkipped = await _parameter.skippableObj.OnStart(_parameter.args, linkedCts.Token).SuppressCancellationThrow();

        // スキップされたかで処理が分岐
        if (isSkipped)
        {
            // スキップ時の処理
            await _parameter.skippableObj.OnSkip(_parameter.args, _parameter.parentCt);
            Debug.Log("スキップされました。");
        }
        else
        {
            // スキップされなかった時の処理
            Debug.Log("スキップされませんでした。");
        }

        // イベントを解除
        _disposable?.Dispose();
    }
}


実装方法

カウントダウン演出を例に実際に組み込んでみました。


通常終了


スキップ時

インタフェース側(演出側)

スキップで飛ばされる演出を実装したクラスには、ISkippableインタフェースを実装します。
今回で言えばカウントダウン演出がスキップ可能な演出物になるので、下記のSkippableCountDownにISkippableを実装しています。

なお、ISkippableはジェネリックになっており、任意のクラスを指定することができます。
ここで指定するクラスは後述のOnStart()とOnSkip()に渡すことを想定しており、必要になる変数をまとめたクラス(Args)を定義しています。
今回の例では、カウントする秒数と処理完了時の表示文字列が外部クラスから渡されるため、それらを定義しています。
(特に必要なければ空でもOK)

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

/*
 * 
 *  @ カウントダウン演出の制御クラス
 *  
 */
public class SkippableCountDown : MonoBehaviour, ISkippable<SkippableCountDown.Args>
{
  // OnStartとOnSkipに渡したい変数群
    public class Args 
    {
        //! カウント秒数
        public int countTime;

        //! 通常終了時のテキスト
        public string defaultCompleteText = "";

        //! スキップ時のテキスト
        public string skipCompleteText = "";

        // コンストラクタ
        public Args(int countTime, string defaultCompleteText, string skipCompleteText)
        {
            this.countTime = countTime;
            this.defaultCompleteText = defaultCompleteText;
            this.skipCompleteText = skipCompleteText;
        }
    }

    //! 状態テキスト
    [SerializeField] protected TextMeshProUGUI _stateText = null;


    // メインの処理の開始
    public async UniTask OnStart(Args args, CancellationToken cancellationToken)
    {
        // キャンセル確認
        cancellationToken.ThrowIfCancellationRequested();

        for (int i = args.countTime; i >=0; i--)
        {
            _stateText.text = (i).ToString();

            await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: cancellationToken);
        }

        _stateText.text = args.defaultCompleteText;
    }

    // スキップされたときの処理
    public async UniTask OnSkip(Args args, CancellationToken cancellationToken)
    {
        // キャンセル確認
        cancellationToken.ThrowIfCancellationRequested();

        _stateText.text = args.skipCompleteText;
        await UniTask.CompletedTask;
    }
}

ISKippableを実装すると、OnStart()OnSkip() を実装することを求められます。

  • OnStart
    ... 通常の演出フローを実装する。今回の例ではテキストの変更を行っている。

  • OnSkip
    ... スキップ時にのみ呼ばれる関数。スキップ時に行いたい処理を実装する。今回は専用のテキストへの変更を行っている。

呼び出し側

スキップ可能な演出を呼び出す側が行うことは、SkipManagerのインスタンス化 と、SKipManagerのProcess() を呼び出すことです。

また、SkipManagerもジェネリックになっており、こちらで指定するのはISkippableで指定したクラスと同一のものを指定します。

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;
using TMPro;


/*
 * 
 *  @ 演出の呼び出し側クラス
 *  
 */
public class UIController : MonoBehaviour
{
    //! カウントダウン演出
    [SerializeField] protected SkippableCountDown _countDown = null;

    //!
    [SerializeField] protected Button _skipButton = null;

    private void Start()
    {
        StartCountDown().Forget();
    }

    /*
     *  @ カウントダウン演出
     */
    private async UniTask StartCountDown()
    {
        // スキップ処理に必要なパラメータの作成
        SkipManager<SkippableCountDown.Args>.Parameter parameter = new SkipManager<SkippableCountDown.Args>.Parameter
            (
                skippableObj: _countDown,
                args: new SkippableCountDown.Args(5, "DefaultComplete", "SkipComplete"),
                skipTrigger: _skipButton.OnClickAsObservable(),
                parentCt: _countDown.GetCancellationTokenOnDestroy()
            );
        SkipManager<SkippableCountDown.Args> skipManager = new SkipManager<SkippableCountDown.Args>(parameter);

        // 演出開始
        await skipManager.Process();

	// 演出終了後の処理
        Debug.Log("終了");
    }
}

インスタンス化するときに渡すパラメータ群の説明は以下。

  • skippableObj
    ... ISkippableを実装したクラス。今回で言えばカウントダウン制御クラス。

  • args
    ... OnStartとOnSkipで使用する変数群。(ジェネリックで定義したクラス)

  • skipTrigger
    ... スキップを起動させるIObservable。基本的にはボタンの押下か画面タップになる想定。

  • parentCt
    ... 任意のCanacellationToken。演出物がProcess()の起動ちゅうなどに消失したときにUniTaskを止めれるように念のため。基本的にはISkippableを実装しているクラスのGetCancellationTokenOnDestroy()になる想定。

おわりに

実装側も呼び出し側もかなり組み込みの手間を省けるような実装ができたんじゃないかなと思ってます。
ただ、自己満足的に作ったのでこの記事の説明はかなり雑です。申し訳ありません。

何かの参考になれば幸いです。

Discussion