【Unity】Input Systemで連打防止を実現するInteraction

こじゃらこじゃら

Input Systemを使ってボタンの連打を防ぐにはどうすればいいの?

このはこのは

やり方はいくつかあるけど、Interactionを自作すれば応用が効いて便利だわ。

Input Systemでボタンを短い間隔で連打できないようにする方法の解説記事です。

本記事では、ボタンが押されたら一定時間だけ入力を受け付けなくするものとします。

実現方法は一通りではありませんが、Input SystemのInteractionを使用すると、次のメリットが得られます。

Interactionで連打防止を実装するメリット
  • 連打防止のロジックとそれ以外のゲームロジックを綺麗に分離できる
  • 複数のInput Actionに流用できる

Interactionにはいくつかプリセットが提供されていますが、連打防止専用のInteractionは存在しない [1] ため、自作して対応するものとします。

本記事では、このような連打防止Interactionの実装例を紹介します。内容を読み進めるにあたり、Input SystemおよびInput Actionに関する基礎知識が必要になりますので予めご了承ください。

動作環境
  • Unity 2022.2.5f1
  • Input System 1.4.4

スポンサーリンク

前提条件

Input Systemパッケージがインストールされ、有効化されているものとします。

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

また、Interactionを使用するためには、Input Actionを使用する必要があります。Input Actionの使い方は以下記事で解説しています。

Interactionの仕組み

Input SystemのInteraction特定の入力パターンを表すための機能です。例えば、ボタンの長押しやダブルタップなどがこれに該当します。

内部的には次の5状態(Phase)からなるステートマシンとして管理されます。

Interactionの状態一覧
  • Waiting – 入力待ち状態
  • Started – Interactionが開始された状態
  • Performed – Interactionが完了した(期待する入力を満たした)状態
  • Canceled – Interactionがキャンセルされた状態
  • Disabled – Actionが無効な状態

このうちInteraction側が主に使用するのはDisable以外の4状態です。

通常は特定の条件を満たす入力を検知したときにPerformed状態に遷移するようにします。

参考:Interactions | Input System | 1.4.4

連打防止のInteractionの実装方法

次のような状態遷移のInteractionを実装すれば連打防止が実現できます。

Interactionの状態遷移

Performedに遷移するために、次の2つの条件を満たす必要がある点がポイントです。

Performedに遷移する条件
  • 入力値がPress以上
  • 前回のPerformedへの遷移(performedコールバック発火)からn秒以上経過

通常のDefault InteractionのButtonの場合は1つ目の条件しかありませんが、本記事で紹介する連打防止Interactionでは時間経過の条件が追加されています。

Interactionの実装例

以下、連打防止Interactionの実装例です。

PreventMashInteraction.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class PreventMashInteraction : IInputInteraction
{
    // 最小の入力間隔[s](押された後、入力を受け付けない時間[s])
    public float minInputDuration;

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

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

    // 直近のPerformed状態に遷移した時刻
    private double _lastPerformedTime;

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

    public void Process(ref InputInteractionContext context)
    {
        if (context.isWaiting)
        {
            // Waiting状態

            // 入力が0以外かどうか
            if (context.ControlIsActuated())
            {
                // 0以外ならStarted状態に遷移
                context.Started();
            }
        }

        if (context.isStarted)
        {
            // Started状態

            // 入力がPress以上
            //     かつ
            // 前回のPerformed状態遷移から「minInputDuration」以上経過 したかどうか
            if (context.ControlIsActuated(pressPointOrDefault) && context.time >= _lastPerformedTime + minInputDuration)
            {
                // Performed状態に遷移
                context.PerformedAndStayPerformed();

                // Performed状態遷移時の時刻を保持
                _lastPerformedTime = context.time;
            }
            // 入力が0かどうか
            else if (!context.ControlIsActuated())
            {
                // 0ならCanceled状態に遷移
                context.Canceled();
            }
        }

        if (context.phase == InputActionPhase.Performed)
        {
            // Performed状態

            // 入力がRelease以下かどうか
            if (!context.ControlIsActuated(releasePointOrDefault))
            {
                // Canceled状態に遷移
                context.Canceled();
            }
        }
    }

    public void Reset()
    {
    }
}

上記スクリプトをPreventMashInteraction.csという名前でUnityプロジェクトに保存すると機能するようになります。

Interactionの適用例

ここでは、次のようなActionに対して連打防止を適用するものとします。

適用したい対象のActionを選択し、Action PropertiesInteractions右の+アイコンをクリックし、Pervent Mash [2] を選択します。

すると、以下のように自作のInteractionが追加されますので、必要に応じてパラメータを調整してください。

例ではボタンが押されてから入力を受け付けない時間(Min Input Duration)を1秒としました。Press Pointはデフォルト値を示す0としました。 [3]

設定が済んだら忘れずにSave AssetボタンをクリックしてInput Actionの設定内容を保存してください。

テスト用スクリプトの実装

本記事では、正しくInteractionが適用されたか確認するために、テスト用スクリプトから入力受け取りの確認を行うものとします。こちらは必須の手順ではないため、不要ならスキップして構いません。

以下、Player Input経由でボタン入力を受け取る例です。

CheckSubmitExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class CheckSubmitExample : MonoBehaviour
{
    // Player Inputから経由で受け取るコールバック
    public void OnSubmit(InputAction.CallbackContext context)
    {
        // コールバックを受け取ったら、Phase(Interactionの状態)をログ出力
        Debug.Log($"Submit action phase = {context.phase}");
    }
}

上記スクリプトをCheckSubmitExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチしておきます。

入力を受け取れるようにするには、次に解説するPlayer Inputの設定が必要です。

Player Inputの設定

前述のチェック用スクリプトに対して入力を渡せるように設定します。本記事ではPlayer Input経由でInput Actionの入力をスクリプトに渡すこととします。

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

まず、適当なゲームオブジェクトにPlayer Inputコンポーネントをアタッチします。

インスペクターよりPlayer InputコンポーネントのActions項目に該当するInput Action Assetを指定し、BehaviourInvoke Unity Eventsを指定し、Events以下の該当するAction(例ではSubmit)にメソッドを登録します。

例ではUIのMap配下のActionを使用するため、Default MapをUIに設定しています。

実行結果

ボタンを連打しても一定間隔より短くperformedコールバックが発火しないようになりました。

startedコールバックは押される度に、canceledコールバックは離される度に発火するようにしているため、連打するとperformedコールバックだけが少ない回数になることが特徴です。

スクリプトの解説

Interactionの実装は、IInputInteractionインタフェースを実装することで行います。IInputInteractionはUnityEngine.InputSystem名前空間に属するため、次のようにusingしてインタフェースを実装しています。

using UnityEngine;
using UnityEngine.InputSystem;

public class PreventMashInteraction : IInputInteraction

参考:Interface IInputInteraction | Input System | 1.4.4

Interactionの設定項目は次のpublicフィールドとして定義します。

// 最小の入力間隔[s](押された後、入力を受け付けない時間[s])
public float minInputDuration;

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

受け取った入力の処理はProcessメソッドで行います。この中でInteractionのPhaseの状態遷移を記述していきます。

public void Process(ref InputInteractionContext context)

Processメソッドはコントローラーなどの入力値が変更されるたびに呼ばれます。

参考:Interface IInputInteraction | Input System | 1.4.4

Processメソッドの以下部分にて、Waiting状態のときに0以外が入力されたときにStartedに遷移する処理を行っています。

if (context.isWaiting)
{
    // Waiting状態

    // 入力が0以外かどうか
    if (context.ControlIsActuated())
    {
        // 0以外ならStarted状態に遷移
        context.Started();
    }
}

context.isWaitingプロパティでInteractionがWaiting状態かを判定しています。

次の書き方でも同様の判定ができます。

if (context.phase == InputActionPhase.Waiting)

context.ControlIsActuatedメソッド入力値の大きさが0より大きいかどうかを判定し、条件が真ならcontext.Startedメソッド呼び出しでStarted状態に遷移しています。

参考:Struct InputInteractionContext | Input System | 1.4.4

ポイント

Phaseの遷移が発生すると、コールバックが呼び出されます。例えばPhaseがWaitingからStartedに遷移すると、startedコールバックが呼び出されます。

Startedからの状態遷移は以下で行っています。

if (context.isStarted)
{
    // Started状態

    // 入力がPress以上
    //     かつ
    // 前回のPerformed状態遷移から「minInputDuration」以上経過 したかどうか
    if (context.ControlIsActuated(pressPointOrDefault) && context.time >= _lastPerformedTime + minInputDuration)
    {
        // Performed状態に遷移
        context.PerformedAndStayPerformed();

        // Performed状態遷移時の時刻を保持
        _lastPerformedTime = context.time;
    }
    // 入力が0かどうか
    else if (!context.ControlIsActuated())
    {
        // 0ならCanceled状態に遷移
        context.Canceled();
    }
}

context.ControlIsActuatedメソッド引数に閾値を渡せるため、ボタンが押された判定となる閾値Press Pointを渡しています。また、入力された時の時刻context.timeを用い、前回のボタン押下から指定時間経過しているかを判定する条件が追加されています。

両方の条件を満たした時だけcontext.PerformedAndStayPerformedメソッドを呼び出してPerformed状態に遷移させます。

入力が0に戻ったら、処理を中断するためにcontext.Canceledメソッド呼び出しでCanceled状態に遷移させます。

Performed状態では、ボタンが離された時にCanceled状態に遷移させるようにしています。

if (context.phase == InputActionPhase.Performed)
{
    // Performed状態

    // 入力がRelease以下かどうか
    if (!context.ControlIsActuated(releasePointOrDefault))
    {
        // Canceled状態に遷移
        context.Canceled();
    }
}

ボタンが離された判定に用いる閾値には、Press Pointではなくそれより小さな値のRelease Pointを使います。これは値のぶれによってボタンが押されているにも関わらず離された判定になるのを防ぐためです。

参考:Class InputSettings | Input System | 1.4.4

さいごに

Input Systemで連打防止を実現する方法は様々ですが、ゲームオブジェクトやUIの状態に依存しないような場合ではInteraction側で判定させることが可能です。

Input SystemのInteractionは、内部的にはステートマシンとして管理されるため、他にも長押しやダブルタップなど特定の入力パターンを検知するのに適しています。

Interactionにはいくつかプリセットも提供されていますので、これらと併せて必要に応じてInteractionを実装すれば、あらゆる入力パターンの検知に対応できるようになるでしょう。

関連記事

参考サイト

スポンサーリンク