【Unity】Input Systemで連打を判定するInteraction

こじゃらこじゃら

Input Systemでボタンが連打されている事を調べる方法はないの?

このはこのは

Interactionを自作すれば可能だわ。Interactionにする事で色々な場所で連打判定を手軽にできるようになるわ。

Input Systemでボタン連打を判定する方法の解説記事です。

実現方法は一通りではありませんが、Input System環境下ではInteractionとして連打判定させることが出来ます。

Interactionは長押しやマルチタップなど特定の入力パターンを判定する役割を持ちます。

参考:Interactions | Input System | 1.5.1

Interactionにはこのようなプリセットが幾つか存在しますが、連打され続けているかどうかを判定するInteractionは存在しません。

本記事では、次のような挙動の連打判定Interactionを自作する方法を紹介します。

実現する挙動
  • 連打されるたびにperformedコールバックを通知する
  • ボタンの連打時間間隔の最大値を指定可能
  • ボタンが連打された回数を取得可能
動作環境
  • Unity 2022.3.0f1
  • Input System 1.5.1

スポンサーリンク

前提条件

事前にInput Systemがインストールされ、有効化されているものとします。

ここまでの手順がわからない方は、以下記事を参考にセットアップを済ませてください。

また、本記事では解説する方法はInteractionを用いて連打判定のタイミングを取得する必要があるため、Input Actionのコールバック経由で入力を取得するのが必須です。

Input Actionの基本的な使い方は以下記事で解説しています。

コールバックの仕組みについては以下記事で解説しています。

本記事で紹介する連打判定Interactionは、Input Systemが提供するMulti Tap Interactionの実装を参考に独自にカスタマイズしたものとなります。

参考:Class MultiTapInteraction| Input System | 1.5.1

連打判定するInteractionの実装方法

Input Systemの Interactionでは、内部的に次の5つのフェーズ(状態)を持つステートマシンとして管理されます。

Interactionのフェーズ
  • Waiting – 入力待ち状態
  • Started – Interactionが開始された状態
  • Performed – Interactionが期待する入力を満たした状態
  • Canceled – Interactionがキャンセルされた状態
  • Disable – Actionが無効な状態

ただし、Interactionが使用するのはDisableを除く4フェーズです。

参考:Interactions | Input System | 1.5.1

本記事の要件を満たす連打判定を行うInteractionは、次のようなフェーズの遷移を行えば実現できます。

Interactionのフェーズ遷移

t秒以内にボタン変化(押された状態と離された状態の変化)が無ければ連打終了とみなします。

上記のボタンが押された判定ですが、これはフェーズとは別にボタンフェーズとして別のステートマシンで実現できます。

ボタンフェーズの遷移

入力値の大きさの変化をチェックし、入力無しから有りに変化するたびに連打回数を加算していくようにします。通常は回数を保持する変数をインクリメントすれば良いです。

そして、1つ目のフェーズの遷移でCanceledに移行したら、連打終了として連打回数をリセット(0に初期化)します。

本記事では、ボタンが押された瞬間、すなわちWaitingForReleaseに遷移する時に連打回数を加算してボタンが押された判定とします。

連打判定を行うInteractionの実装例

前述のフェーズ、ボタンフェーズの遷移に基づいて連打判定を行うInteractionの実装例です。

MashInteraction.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class MashInteraction : IInputInteraction
{
    // 各タップの最大許容時間間隔[s]
    public float tapDelay;

    // 入力判定の閾値(0でデフォルト値)
    public float pressPoint;

    // 連打判定に必要な回数
    public int requiredTapCount = 2;

    // 連打された回数
    public int TapCount { get; private set; }

    // 設定値かデフォルト値の値を格納するフィールド
    private float PressPointOrDefault => pressPoint > 0 ? pressPoint : InputSystem.settings.defaultButtonPressPoint;
    private float ReleasePointOrDefault => PressPointOrDefault * InputSystem.settings.buttonReleaseThreshold;
    private float TapDelayOrDefault => tapDelay > 0 ? tapDelay : InputSystem.settings.multiTapDelayTime;

    // ボタンフェーズ
    private enum ButtonPhase
    {
        None,
        WaitingForNextRelease,
        WaitingForNextPress,
    }

    private ButtonPhase _currentButtonPhase;
    private int _remainingRequiredTapCount;

    /// <summary>
    /// 初期化
    /// </summary>
#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#else
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
#endif
    public static void Initialize()
    {
        // 初回にInteractionを登録する必要がある
        InputSystem.RegisterInteraction<MashInteraction>();
    }

    public void Process(ref InputInteractionContext context)
    {
        // タイムアウトチェック
        if (context.timerHasExpired)
        {
            // 最大許容時間以上ボタン変化が無かったら、連打終了とみなす
            context.Canceled();
            return;
        }

        switch (_currentButtonPhase)
        {
            case ButtonPhase.None:
                // 入力され始めた
                if (context.ControlIsActuated(PressPointOrDefault))
                {
                    // ボタンを離すまで待機
                    _currentButtonPhase = ButtonPhase.WaitingForNextRelease;

                    // 残りのタップ回数を初期化
                    _remainingRequiredTapCount = requiredTapCount - 1;

                    // Startedフェーズに移行
                    context.Started();

                    // 必要タップ回数以上タップされたら連打判定とする
                    if (_remainingRequiredTapCount <= 0)
                    {
                        TapCount++;

                        // Performedフェーズに移行
                        context.PerformedAndStayPerformed();
                    }

                    // タイムアウトを設定
                    context.SetTimeout(TapDelayOrDefault);
                }

                break;

            case ButtonPhase.WaitingForNextRelease:
                if (!context.ControlIsActuated(ReleasePointOrDefault))
                {
                    // ボタンを押すまで待機
                    _currentButtonPhase = ButtonPhase.WaitingForNextPress;
                }

                break;

            case ButtonPhase.WaitingForNextPress:
                if (context.ControlIsActuated(PressPointOrDefault))
                {
                    // ボタンを離すまで待機
                    _currentButtonPhase = ButtonPhase.WaitingForNextRelease;

                    // 必要タップ回数に満たなければ、残りの必要タップ回数を更新
                    if (_remainingRequiredTapCount > 0)
                    {
                        _remainingRequiredTapCount--;
                    }

                    // 必要タップ回数以上タップされたら連打判定とする
                    if (_remainingRequiredTapCount <= 0)
                    {
                        // 連打回数をカウント
                        TapCount++;

                        // Performedフェーズに移行
                        context.PerformedAndStayPerformed();
                    }

                    // タイムアウトを設定
                    context.SetTimeout(TapDelayOrDefault);
                }

                break;
        }
    }

    public void Reset()
    {
        _currentButtonPhase = ButtonPhase.None;
        TapCount = 0;
    }
}

上記をMashInteraction.csという名前でUnityプロジェクトに保存すると、Interactionが使えるようになります。

入力を受け取るスクリプトの実装(必要ならば)

本記事では、次のようにInput Actionのコールバックを受け取ってログ出力するスクリプトから連打判定結果を受け取るものとします。あくまで使用例のため必須ではありません。

UseExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class UseExample : MonoBehaviour
{
    [SerializeField] private InputAction _action;

    private void Awake()
    {
        _action.started += OnAction;
        _action.performed += OnAction;
        _action.canceled += OnAction;
    }

    private void OnDestroy()
    {
        _action.started -= OnAction;
        _action.performed -= OnAction;
        _action.canceled -= OnAction;

        _action.Dispose();
    }

    private void OnEnable()
    {
        _action.Enable();
    }

    private void OnDisable()
    {
        _action.Disable();
    }

    private void OnAction(InputAction.CallbackContext context)
    {
        switch (context.phase)
        {
            case InputActionPhase.Started:
                print("ボタンが押された!");
                break;

            case InputActionPhase.Performed:
                print("ボタンが連打された!");
                break;

            case InputActionPhase.Canceled:
                print("連打がキャンセルされた!");
                break;
        }
    }
}

UseExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチすると機能します。

アタッチすると、インスペクターからActionを設定できるようになるため、Bindingを設定してください。

Interactionの適用

連打判定を行いたいInput ActionのInteractionにMash Interactionを適用します。

該当するActionまたはBindingをダブルクリックし、Action PropertyのInteraction右の+アイコンをクリックし、Mashを選択します。

すると、次のように連打判定を行うMash Interactionが追加されるので、必要に応じて項目を設定してください。

  • Tap Delay – タップの最大許容時間間隔[s]。この時間を超えてタップが無いと連打終了とみなす。0はInput System側のデフォルト値。
  • Press Point – ボタンを押したとみなす入力値の大きさの閾値。0の時はデフォルト値となる。
  • Required Tap Count連打判定に必要な最低回数。この回数以上ボタンが連打されたら連打判定となる。

実行結果

ボタンを短い時間間隔(Tap Delayに設定した時間以内)で連打すると、コンソールログに連打の旨のメッセージが出力されます。

連打するたびにperformedコールバックが実行されて連打メッセージが表示されます。

ボタンを一定時間操作(押したり離したり)しなければ、canceledコールバックが実行されて終了します。

スクリプトの説明

Interactionの設定パラメータは、以下のようにpublicフィールドとして定義しています。

// 各タップの最大許容時間間隔[s]
public float tapDelay;

// 入力判定の閾値(0でデフォルト値)
public float pressPoint;

// 連打判定に必要な回数
public int requiredTapCount = 2;

ボタンが押されたとみなす閾値Press Pointやタップの最大許容時間間隔などは、0ならデフォルト値を使うように以下プロパティが管理しています。

// 設定値かデフォルト値の値を格納するフィールド
private float PressPointOrDefault => pressPoint > 0 ? pressPoint : InputSystem.settings.defaultButtonPressPoint;
private float ReleasePointOrDefault => PressPointOrDefault * InputSystem.settings.buttonReleaseThreshold;
private float TapDelayOrDefault => tapDelay > 0 ? tapDelay : InputSystem.settings.multiTapDelayTime;

また、ボタンの状態変化を管理するために、次のように独自のボタンフェーズをenumフィールドで定義しています。

// ボタンフェーズ
private enum ButtonPhase
{
    None,
    WaitingForNextRelease,
    WaitingForNextPress,
}

private ButtonPhase _currentButtonPhase;

ある一定時間以上ボタン変化が無かったら連打終了とする判定は、次の処理で行っています。

// タイムアウトチェック
if (context.timerHasExpired)
{
    // 最大許容時間以上ボタン変化が無かったら、連打終了とみなす
    context.Canceled();
    return;
}

context.timerHasExpiredプロパティは、予め設定しておいたタイムアウト時間を過ぎたときにtrueを返します。

参考:Struct InputInteractionContext| Input System | 1.5.1

タイムアウト時間はボタンが押されるたびにcontext.SetTimeoutメソッドで設定するようにします。

// タイムアウトを設定
context.SetTimeout(TapDelayOrDefault);

参考:Struct InputInteractionContext| Input System | 1.5.1

待機状態からボタンが押される(入力値がPress Point以上になる)と、ボタンフェーズを「ボタンを離すまで待機」する状態に移行させます。

入力値が閾値以上かどうかの判定は、context.ControlIsActuatedメソッドで行えます。trueなら閾値以上(ただし閾値0なら0より大きい)、falseなら閾値未満(ただし閾値0の場合は入力値0)と判断できます。

参考:Struct InputInteractionContext| Input System | 1.5.1

case ButtonPhase.None:
    // 入力され始めた
    if (context.ControlIsActuated(PressPointOrDefault))
    {
        // ボタンを離すまで待機
        _currentButtonPhase = ButtonPhase.WaitingForNextRelease;

        // 残りのタップ回数を初期化
        _remainingRequiredTapCount = requiredTapCount - 1;

        // Startedフェーズに移行
        context.Started();

        // 必要タップ回数以上タップされたら連打判定とする
        if (_remainingRequiredTapCount <= 0)
        {
            TapCount++;

            // Performedフェーズに移行
            context.PerformedAndStayPerformed();
        }

        // タイムアウトを設定
        context.SetTimeout(TapDelayOrDefault);
    }

    break;

この時、連打に必要な残りのタップ回数_remainingRequiredTapCountを初期化したり、フェーズをStartedに移行したりしています。

ボタンが押されたときに_remainingRequiredTapCountが0以下になったら連打と判定し、連打回数TapCountをインクリメントでカウントしています。

ボタンが押された状態から離されたかを判定する処理は以下部分です。

case ButtonPhase.WaitingForNextRelease:
    if (!context.ControlIsActuated(ReleasePointOrDefault))
    {
        // ボタンを押すまで待機
        _currentButtonPhase = ButtonPhase.WaitingForNextPress;
    }

    break;

ここでボタンフェーズを「ボタンが押されるまで待機」する状態に移行しています。

case ButtonPhase.WaitingForNextPress:
    if (context.ControlIsActuated(PressPointOrDefault))
    {
        // ボタンを離すまで待機
        _currentButtonPhase = ButtonPhase.WaitingForNextRelease;

        // 必要タップ回数に満たなければ、残りの必要タップ回数を更新
        if (_remainingRequiredTapCount > 0)
        {
            _remainingRequiredTapCount--;
        }

        // 必要タップ回数以上タップされたら連打判定とする
        if (_remainingRequiredTapCount <= 0)
        {
            // 連打回数をカウント
            TapCount++;

            // Performedフェーズに移行
            context.PerformedAndStayPerformed();
        }

        // タイムアウトを設定
        context.SetTimeout(TapDelayOrDefault);
    }

    break;

連打判定のロジックは、NoneからWaitingForNextReleaseに遷移する時と一緒です。

連打判定されたら、フェーズをPerformedに移行し、連打終了するまでPerformedに留まらせます。

参考:Struct InputInteractionContext| Input System | 1.5.1

この時も忘れずにcontext.SetTimeoutメソッドでタイムアウト設定する必要があります。

連打された回数を取得する

応用例として、連打された回数を取得する例を紹介します。

入力の受取り側で変数管理しても良いですが、本記事では連打回数をInteraction側で管理するようにします。

前述のMash Interactionでは、TapCountプロパティから連打回数を取得できるようになっています。これを次のようにコールバック引数からInteraction経由で取得できます。

private void OnPerformed(InputAction.CallbackContext context)
{
    if (context.interaction is not MashInteraction mashInteraction) return;

    // 連打回数を表示
    print($"連打回数 : {mashInteraction.TapCount}");
}
メモ

コールバックで受け取る引数contextのinteractionプロパティより実際に反応したInteractionを取得できます。

必要に応じてキャストして使います。

参考:Struct InputAction.CallbackContext| Input System | 1.5.1

サンプルスクリプト

以下、連打回数をスクリプトから取得してログ出力する例です。

TapCountExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class TapCountExample : MonoBehaviour
{
    // 連打判定Interactionが指定されていると想定するAction
    [SerializeField] private InputAction _action;

    private void Awake()
    {
        // Performedコールバックのみ登録
        _action.performed += OnPerformed;
    }

    private void OnDestroy()
    {
        _action.performed -= OnPerformed;

        _action.Dispose();
    }

    private void OnEnable()
    {
        _action.Enable();
    }

    private void OnDisable()
    {
        _action.Disable();
    }

    private void OnPerformed(InputAction.CallbackContext context)
    {
        // 連打判定Interactionをキャストして取得
        // キャストに失敗したら何もしない
        if (context.interaction is not MashInteraction mashInteraction) return;

        // 連打回数を表示
        print($"連打回数 : {mashInteraction.TapCount}");
    }
}

上記をTapCountExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、ActionよりBindingとMash Interactionを設定してください。

実行結果

Actionに登録したボタンを連打すると、その連打回数がログ出力されます。

さいごに

Input Systemでボタン連打されたかどうかの判定は、Interactionを自作することでスマートに実現できます。

特に連打判定などのように、時系列での入力パターンを判定する用途にInteractionは適しています。

Interaction内部では、状態遷移とタイムアウトを駆使して実装するのがポイントです。

関連記事

参考サイト

スポンサーリンク