【Unity】Input Systemでキーリピートを実現するInteraction

こじゃらこじゃら

Input Systemでボタンを押しっぱなしにした時、「タン、タタタタタン」と反応させるようにしたいの。

このはこのは

キーリピートだね。やり方は色々あるけどInteractionを作って実装する方法を解説していくね。

Input Systemでキーリピートを実現する方法の解説記事です。

本記事で解説するキーリピートは、ボタンを押しっぱなしにすると、押してからワンクッション置いて連打されるような挙動を想定しています。

実装方法は一通りではありませんが、Input Systemの機能であるInteractionを自作して実現すると、次のようなメリットがあります。

主なメリット
  • 様々なボタン操作(Action)などに対してキーリピート設定を適用可能になる
  • ゲームロジックと入力ロジックを綺麗に分離できる
  • スティック入力などボタン以外にも適用可能

Input SystemのInteractionは、長押しやダブルタップなど、特定の入力パターンを検知するための仕組みで、この仕組みに則ってキーリピートを実現できると使いまわしが効くようになります。

参考:Interactions | Input System | 1.7.0

本記事では、このようなキーリピートを行うInteractionを自作する方法を解説していきます。

注意

本記事で解説する方法は、あくまでもキーリピート処理を独自実装するものであり、OS側の設定を反映するものではない点をご了承ください。

動作環境
  • Unity 2023.2.16f1
  • Input System 1.7.0

スポンサーリンク

前提条件

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

導入手順は以下記事で解説しています。

また、本記事を読み進めるにあたっては、Input SystemのInteractionの基本を押さえておくと理解がスムーズです。

Interactionの基本については以下記事で解説しています。

キーリピート処理の実現方法

ボタンを押しっぱなしにした時、最初に反応し、ワンクッション置いてから連打するように反応させるものとします。

キーリピートの挙動

押した瞬間から自動連打が開始しますが、1回目から2回目までの間隔は長め2回目以降は短めの間隔で発火させるようにすれば良いです。

Input SystemのInteractionでは、ボタンが押された瞬間や長押しされた瞬間など、特定の入力パターンの条件を満たした時にperformedイベントが発火されるようになっています。

このボタンの反応するタイミングでperformedイベントを発火させれば、Interactionでキーリピートが実現できます。

イベント発火のタイミング

また、入力の開始終了ではそれぞれstartedcanceledイベントを発火させる必要があります。これはボタンが押された瞬間にstarted離された瞬間にcanceledイベントを発火させればよいでしょう。

したがって、最終的なイベント発火タイミングは以下のようになります。

最終的なイベント発火タイミング

ボタンの押された/離された判定

ボタン入力値は実際には0~1の連続値で得られる可能性があります。 [1]

そのため、入力値がある閾値(Press point)以上なら「押された」ある閾値(Release Point)以下なら「離された」と閾値と比較して押下判定する必要があります。 [2]

参考:Input settings | Input System | 1.7.0

Interactionは内部的には5つの状態(フェーズ)を持つステートマシンとして振る舞います。

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

このうち、主に使用するのはDisable以外の4状態です。イベント発火はStarted、Performed、Canceledフェーズへの遷移時にそれぞれstarted、performed、canceledイベントが発火するようになっています。

したがって、キーリピートを実装するInteractionでは、次のようなフェーズ遷移をさせれば良いことになります。

Interactionのフェーズ遷移

Performedフェーズに限っては、同じPerformedフェーズに遷移させることが可能です。

参考:Struct InputInteractionContext | Input System | 1.7.0

実装例

以下、キーリピート判定を行うInteractionの実装例です。

KeyRepeatInteraction.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class KeyRepeatInteraction : IInputInteraction
{
    // ボタンが最初に押されてからリピート処理が始まるまでの時間[s]
    public float repeatDelay = 0.5f;

    // ボタンが押されている間のリピート処理の間隔[s]
    public float repeatInterval = 0.1f;

    // ボタンの閾値(0の場合はデフォルト設定値を使用)
    public float pressPoint = 0;

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

    // 次のリピート時刻[s]
    private double _nextRepeatTime;

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

    public void Process(ref InputInteractionContext context)
    {
        // 設定値のチェック
        if (repeatDelay <= 0 || repeatInterval <= 0)
        {
            Debug.LogError("initialDelayとrepeatIntervalは0より大きい値を設定してください。");
            return;
        }

        if (context.timerHasExpired)
        {
            // リピート時刻に達したら再びPerformedに遷移
            if (context.time >= _nextRepeatTime)
            {
                // リピート処理の次回実行時刻を設定
                _nextRepeatTime = context.time + repeatInterval;

                // リピート時の処理
                context.PerformedAndStayPerformed();

                // 次のリピート時刻にProcessメソッドが呼ばれるようにタイムアウトを設定
                context.SetTimeout(repeatInterval);
            }

            return;
        }

        switch (context.phase)
        {
            case InputActionPhase.Waiting:
                // ボタンが押されたらStartedに遷移
                if (context.ControlIsActuated(PressPointOrDefault))
                {
                    // ボタンが押された時の処理
                    context.Started();
                    context.PerformedAndStayPerformed();

                    // リピート処理の初回実行時刻を設定
                    _nextRepeatTime = context.time + repeatDelay;

                    // 次のリピート時刻にProcessメソッドが呼ばれるようにタイムアウトを設定
                    context.SetTimeout(repeatDelay);
                }

                break;

            case InputActionPhase.Performed:
                // ボタンが離されたらCanceledに遷移
                if (!context.ControlIsActuated(ReleasePointOrDefault))
                {
                    // ボタンが離された時の処理
                    context.Canceled();
                }

                break;
        }
    }

    public void Reset()
    {
        _nextRepeatTime = 0;
    }
}

上記スクリプトをUnityプロジェクトに保存すると、Key Repeatという独自のInteractionとして使用可能になります。

キーリピートInteractionが使用可能になっている様子

入力確認用スクリプト(必要ならば)

本記事では、キーリピート入力の挙動を確認するために、次のスクリプトを用いるものとします。あくまで確認用のため、必須の手順ではありません。

KeyRepeatExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class KeyRepeatExample : MonoBehaviour
{
    // キーリピート入力を受け付けるAction
    [SerializeField] private InputAction _action;

    private void Awake()
    {
        // コールバックを登録
        _action.started += OnStarted;
        _action.performed += OnPerformed;
        _action.canceled += OnCanceled;
    }

    private void OnDestroy()
    {
        // コールバックを解除
        _action.started -= OnStarted;
        _action.performed -= OnPerformed;
        _action.canceled -= OnCanceled;
        
        // InputActionの解放
        _action.Dispose();
    }

    private void OnEnable() => _action.Enable();
    private void OnDisable() => _action.Disable();

    // イベントを受け取ったらログ出力
    private void OnStarted(InputAction.CallbackContext context)
        => print("Started");

    private void OnPerformed(InputAction.CallbackContext context)
        => print("Performed");

    private void OnCanceled(InputAction.CallbackContext context)
        => print("Canceled");
}

上記をKeyRepeatExample.csというファイル名でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりActionの設定を行えば入力を受け取れるようになります。

例ではキーボードの「A」キーを設定するものとします。

Interactionの適用

前述のキーリピートInteractionを、適用したいActionまたはBindingに設定します。

例では、前述のサンプルスクリプトの「A」キーのBindingに設定するものとします。

本記事で提示したキーリピートInteractionでは、次のパラメータをインスペクターから設定できるようにしました。必要に応じて調整してください。

  • Repeat Delay – リピートが開始されるまでの時間[s]
  • Repeat Interval – リピート間隔[s]
  • Press Point – ボタンの押された判定の閾値。0ならInput Settings側のデフォルト設定値を参照

実行結果

キーを押しっぱなしにすると、押した瞬間にログ出力された後、暫くして連打されたようにログ出力されます。分かりやすさのため、画面にキーの押下状態を示しています。

押した瞬間にはstartedperformedが、キーリピート時performedのみがログ出力されていることが確認できます。また、キーを離すcanceledが出力されてログ出力が止まります。

スクリプトの説明

Interactionの基本実装

キーリピートのInteractionを実装するために、以下のようにIInputInteractionインタフェースを実装したクラスを定義します。

public class KeyRepeatInteraction : IInputInteraction

参考:Interface IInputInteraction | Input System | 1.7.0

キーリピートの開始までの時間やリピート間隔は、publicフィールドで定義しています。

// ボタンが最初に押されてからリピート処理が始まるまでの時間[s]
public float repeatDelay = 0.5f;

// ボタンが押されている間のリピート処理の間隔[s]
public float repeatInterval = 0.1f;

// ボタンの閾値(0の場合はデフォルト設定値を使用)
public float pressPoint = 0;

Press Pointが0の時はInput Settingsの設定値 [3] を反映しますが、これは内部的にプロパティとして参照可能にしています。

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

参考:Class InputSettings | Input System | 1.7.0

参考:Class InputSettings | Input System | 1.7.0

キーリピートは独自Interactionとして実装していますが、これはそのままでは使用可能になりません。次のようにInput System側に登録する必要があります。

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

参考:Class InputSystem | Input System | 1.7.0

IInputInteractionインタフェースには、Interactionの実際の処理を行うProcessメソッドと、初期化やリセット時の処理を行うResetメソッドを実装する必要があります。

public void Process(ref InputInteractionContext context)
public void Reset()

参考:Interface IInputInteraction | Input System | 1.7.0

キーリピート処理

前述のProcessメソッド内にキーリピートのInteraction処理を実装していきます。この中で各種フェーズ遷移の処理が行われます。

Waitingフェーズ(入力無し)の時にボタン入力があると、Started→Performedの順にフェーズ遷移させます。

switch (context.phase)
{
    case InputActionPhase.Waiting:
        // ボタンが押されたらStartedに遷移
        if (context.ControlIsActuated(PressPointOrDefault))
        {
            // ボタンが押された時の処理
            context.Started();
            context.PerformedAndStayPerformed();

            // リピート処理の初回実行時刻を設定
            _nextRepeatTime = context.time + repeatDelay;

            // 次のリピート時刻にProcessメソッドが呼ばれるようにタイムアウトを設定
            context.SetTimeout(repeatDelay);
        }

        break;

context.StartedメソッドStartedフェーズへ遷移させます。

context.PerformedAndStayPerformedメソッドPerformedフェーズへ遷移させ、そのままPerformedフェーズに留まらせる操作を行います。

参考:Struct InputInteractionContext | Input System | 1.7.0

参考:Struct InputInteractionContext | Input System | 1.7.0

ボタンが押されたかどうかの判定は、context.ControlIsActuatedメソッドで行います。引数には閾値を取ります。

if (context.ControlIsActuated(PressPointOrDefault))

これは内部的には入力値の大きさと閾値との比較を行っています。 [4]

参考:Struct InputInteractionContext | Input System | 1.7.0

これで押された瞬間の判定はできますが、このままではProcessメソッドが呼ばれない可能性があります。 [5] そのため、一定時間経過したらタイムアウトを発生させ、強制的にProcessメソッドが呼ばれるようにしています。

// 次のリピート時刻にProcessメソッドが呼ばれるようにタイムアウトを設定
context.SetTimeout(repeatDelay);

最初の反応からワンクッション置いて連打開始するまでの処理は以下部分です。

if (context.timerHasExpired)
{
    // リピート時刻に達したら再びPerformedに遷移
    if (context.time >= _nextRepeatTime)
    {
        // リピート処理の次回実行時刻を設定
        _nextRepeatTime = context.time + repeatInterval;

        // リピート時の処理
        context.PerformedAndStayPerformed();

        // 次のリピート時刻にProcessメソッドが呼ばれるようにタイムアウトを設定
        context.SetTimeout(repeatInterval);
    }

    return;
}

context.timerHasExpiredプロパティはタイムアウトが発生したかどうかを返します。

参考:Struct InputInteractionContext | Input System | 1.7.0

これがtrueならタイムアウト発生したとみなせます。本記事のキーリピートInteractionでは、リピートするタイミングでタイムアウトが発生するため、この瞬間にPerformedフェーズに遷移させ、performedイベントを発火させています。

タイムアウトが発生した時点で既にキーリピートは始まっているとみなせるため、次の処理でリピート間隔でタイムアウトを設定しています。

// 次のリピート時刻にProcessメソッドが呼ばれるようにタイムアウトを設定
context.SetTimeout(repeatInterval);

これによってタイムアウトが連続で発生し、キーリピートが実現できます。

ボタンを離したときにInteractionを終了させる処理は、以下部分です。

case InputActionPhase.Performed:
    // ボタンが離されたらCanceledに遷移
    if (!context.ControlIsActuated(ReleasePointOrDefault))
    {
        // ボタンが離された時の処理
        context.Canceled();
    }

    break;

context.ControlIsActuatedメソッド閾値Release pointとの比較を行い、入力値の大きさがこれ以下なら「離された」判定としてcontext.CanceledメソッドCanceledフェーズに遷移させています。

入力値が変化してProcessメソッド呼ばれる場合はタイムアウトとならないため、この処理に到達できます。

参考:Struct InputInteractionContext | Input System | 1.7.0

Event Systemのキー移動について

Event Systemでは、矢印キーやスティックなどで移動するナビゲーション機能が存在します。

このような移動は、Event System側でキーリピートが実現されているため、本記事のような手順は不要です。

ただし、InputActionを直接参照して入力を受け取っている場合はこの限りではありません。

メモ

EventSystemオブジェクトにアタッチされているInputSystemUIInputModuleコンポーネントの以下項目からリピート開始までの時間[s]リピート間隔[s]を設定できます。

参考:UI support | Input System | 1.7.0

参考:Class InputSystemUIInputModule | Input System | 1.7.0

スティック操作などで使用する場合

本記事で解説したキーリピートInteractionは、スティックの2軸入力などボタン以外でも適用可能です。

スティック入力にキーリピートInteractionを適用すると、スティックを倒しっぱなしにしたときに連続で入力を受け取る(performedイベント発火)挙動を取ります。

このような挙動が実現できるのは、入力があったかどうかの判定を「入力値の大きさ」で判定しているためです。

// ボタンが押されたらStartedに遷移
if (context.ControlIsActuated(PressPointOrDefault))

2軸入力値の大きさは、そのベクトルの大きさとなります。 [6]

参考:Struct Vector2MagnitudeComparer | Input System | 1.7.0

さいごに

キーリピート処理はゲームロジック側で実装しても実現可能ですが、Input SystemのInteractionとして実装すると様々な操作(Action)に対して汎用的に使いまわせるようになります。

入力があるかどうかの判定は、「入力値の大きさ」として汎用的に比較できるので、スティックのような2軸入力などにも対応できるメリットを備えています。

関連記事

参考サイト

スポンサーリンク