Unity_Lesson

【Unity】Input Systemの同時押しを排他制御する


例えばTabキーを使ったナビゲーションを実装するとき、Tabキー単体では「進む」操作になるが、Shift + Tabキーが押された場合は「戻る」操作となり、「進む」操作は実行されないようにしたい場合を想定します。

通常、Shift + Tabを同時押しするとTabキーが押された時点で「進む」と「戻る」の両方の操作が実行されるかと思います。

これらを排他制御(一度に一つのプロセスだけが、データを操作できるように制御)する機能が追加されています。

排他制御の有効化設定は、アプリケーション全体に影響を及ぼすため、注意する必要があります。これが許容できない場合はComposite Bindingを自作する必要があります。


排他制御の設定方法

(アプリケーション全体のInput Actionに対して排他制御が適用されます。既存のプロジェクトなどに適用する場合は、影響範囲に注意してください。一部のボタンのみに対して排他制御をかけたい場合、カスタムComposite Bindingを実装する手順で実現可能です。)

初期設定では排他制御は無効化されています。

有効化するには、トップメニューのEdit > Project Settings…を選択してProject Settingsウィンドウを開き、Input System Package > Enable Input Consumptionにチェックを入れます。


Input Actionの設定例

排他制御を検証するための設定例です。

次のようなキー割り当てのActionに対して排他制御を適用してみます。


入力確認

ここでは、「次へ」「前へ」のActionが実行されたときにログ出力するものとします。
入力取得はPlayer Inputコンポーネント経由で行うものとします。

以下、取得例です。

GetInputsExample.cs

using UnityEngine;
using UnityEngine.InputSystem;

public class GetInputsExample : MonoBehaviour
{
    // 「次へ」のAction
    public void OnNext(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間(performedコールバック)のみ拾う
        if (!context.performed) return;
            
        // ログ出力
        print("次へ");
    }

    // 「前へ」のAction
    public void OnPrev(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間(performedコールバック)のみ拾う
        if (!context.performed) return;
            
        // ログ出力
        print("前へ");
    }
}

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

Player Inputの設定

前述の確認用スクリプトから入力を取得できるようにするために、Player Input側の設定を行います。

適当なゲームオブジェクトにPlayer Inputコンポーネントをアタッチし、Actions項目に予め作成したInput Actionアセットを、Behaviour項目にInvoke Unity Eventsを指定します。


そして、Events項目の該当するActionにスクリプトのメソッドを登録します。


実行すると、排他制御の適用前は、「前へ」操作をしているにも関わらず「次へ」操作が反応してしまっています。


排他制御を適用すると、「前へ」操作を行なっても「次へ」操作が反応しません。



カスタムComposite Bindingで排他制御を実装する

アプリケーション全体に対して排他制御を実装したくないときなどは、カスタムComposite Bindingを自作して対処することも可能です。

Composite Bindingを用いると、複数の入力を合成したり、ある入力の型を別の型に変換したりすることができます。

あるボタンmodifierが押されている間は入力を流さず、離されている間は入力を流す挙動のComposite Bindingを実装します。


ボタンの入力値はアナログ値ですが、次のように閾値判定によって押されている/離されているのどちらかを判定できます。

ボタンの押下状態判定

こちらの方法は、一つ目の方法とは異なり、個別のActionに対して排他制御を一つ一つ適用していく必要があります。

実装例

排他制御を行うComposite Bindingの実装例です。

DisallowOneModifierComposite.cs

using System.ComponentModel;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;

[DisplayName("Disallow One Modifier")]
public class DisallowOneModifierComposite : InputBindingComposite<float>
{
    // このキーが押されていなければbuttonのActionを実行する
    [InputControl(layout = "Button")] public int modifier;
    
    // 排他制御対象のボタン
    [InputControl(layout = "Button")] public int button;


    /// <summary> 初期化 </summary>
#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#else
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif

    private static void Initialize()
    {
        // 初回にCompositeBindingを登録する必要がある
        InputSystem.RegisterBindingComposite(typeof(DisallowOneModifierComposite), "DisallowOneModifierComposite");
    }
    
    /// <summary>一方のボタンが押されていない時だけ値を返す</summary>
    public override float ReadValue(ref InputBindingCompositeContext context)
    {
        // modifierのボタンが押されていない時だけbuttonの入力を通す
        if (!context.ReadValueAsButton(modifier))
            return context.ReadValue<float>(button);

        return default;
    }
    
    /// <summary>
    /// 入力値の大きさを取得する
    /// modifier入力の押下判定(Press Pointとの閾値判定)のために実装必須
    /// </summary>
    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        return ReadValue(ref context);
    }
}


上記スクリプトをUnityプロジェクトに保存するとComposite Bindingが使用可能になります。

このComposite Bindingを排他制御を適用したいAction(例ではNext)に適用します。


実行結果

(トップメニューのEdit > Project Settings…を選択してProject Settingsウィンドウを開き、Input System Package > Enable Input Consumptionにチェック外しても)
正しく排他制御されるようになりました。「前へ」操作を行なっても「次へ」操作が反応しません。


スクリプトの説明

ボタンの定義。

// このキーが押されていなければbuttonのActionを実行する
[InputControl(layout = "Button")] public int modifier;

// 排他制御対象のボタン
[InputControl(layout = "Button")] public int button;


modifierの入力が無い時に入力を通す処理は以下部分です。

// modifierのボタンが押されていない時だけbuttonの入力を通す
if (!context.ReadValueAsButton(modifier))
    return context.ReadValue<float>(button);

return default;

context.ReadValueAsButtonメソッドで、指定されたBindingをボタンとみなして押下状態を取得しています。

押されている時にtrueが返されるので、押されている時は入力0を、押されていない時はbuttonのBinding入力を経由するとOKです。

ボタンの押下状態の判定は、「入力値の大きさ」と「Press Pointとの閾値判定」をするため、大きさを返すメソッドEvaluateMagnitudeをオーバーライドして実装する必要があります。

public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
    return ReadValue(ref context);
}