【Unity】Input SystemでUI操作との競合を防ぐ方法

こじゃらこじゃら

UIのボタンを押す時だけInput Systemのクリックを反応させたくない場合、どうすればいいの?

このはこのは

カーソルや指の位置がUI上にあるかどうかチェックすれば良いわ。Event Systemのレイキャストなどの機能を活用できるわ。

Unity UI(uGUI)とInput Systemを併用している環境において、UI操作時にマウス左ボタンなどの入力を無効化する方法の解説です。

普通に実装すると、UIのボタンをクリックした時に、Input System側のマウスの左ボタンなどの入力が反応してしまいます。

UI操作と競合している様子

これは、Input System側は特にUI操作関係なしに入力値を返す挙動になっているためです。

このような場合、ボタン入力を取得する際にマウスカーソルや指などがボタンの上にあるかどうかを判定し、上にない時だけ入力を受け付けるようにする必要があります。

また、Input Fieldのキー入力との競合を避けたい場合は、Input Fieldが選択されている場合に入力を受け付けないようにする必要があります。

Input Fieldと競合している様子

本記事では、Unity UIを使用した環境下でUI操作とInput Systemの特定入力を排他的に扱う方法を解説します。

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

スポンサーリンク

前提条件

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

ここまでの導入方法、および基本的な使い方は以下記事で解説しています。

また、本記事ではInput Actionを使用してマウスの左ボタンやタッチ入力などを取得する場面を想定します。Input Actionの基本的な使い方は以下記事で解説しています。

例では、次のようにCanvas上に複数の操作可能なUIが配置されているものとします。

Input System使用化でUI操作を可能にするためには、Event SystemのStandard Input ModuleをInput System UI Input Moduleに置き換える必要があります。

参考:UI support | Input System | 1.7.0

カーソルや指の位置がUI上にあるかどうかの判定

主に次の2通りの方法があります。

  • EventSystem.currentSelectedGameObjectプロパティを使用する
  • EventSystem.RaycastAllメソッドを使用する

前者は、全てのUIを対象としてUI上にカーソルなどがあれば入力をブロックする方法です。そのため、Imageコンポーネントなど操作系意外のUIも対象となります。

次のようなコードでUIの上にあるかどうかを判定できます。

// Input Actionのperformedコールバック
private void OnButtonAction(InputAction.CallbackContext context)
{
    // UIの上にカーソルがあったら、入力を受け付けない
    if (EventSystem.current.IsPointerOverGameObject())
        return;

    print("ボタンが押された!");
}

参考:Method IsPointerOverGameObject | Unity UI | 1.0.0

後者は、特定のUIに限定して判定する方法です。こちらのほうが柔軟性はありますが、レイキャストして得られた要素に対し型チェックを行う処理が走ります。

UIのボタン(Buttonクラス)やスライダー(Sliderクラス)などはSelectableクラスを継承しているため、次のようなコードでレイキャストして得られた結果に対して型チェックを行うことで実現します。

// カーソル位置取得用のAction
InputActionProperty cursorPositionAction;
// レイキャストの結果
List<RaycastResult> _results = new();

・・・(中略)・・・

private void OnButtonAction(InputAction.CallbackContext context)
{
    // レイキャスト用の情報を作成
    var pointer = new PointerEventData(EventSystem.current)
    {
        position = cursorPositionAction.action.ReadValue<Vector2>()
    };

    // カーソル上にあるUIをレイキャストで取得
    results.Clear();
    EventSystem.current.RaycastAll(pointer, results);

    for (var i = 0; i < _results.Count; i++)
    {
        var uiObj = _results[i].gameObject;

        if (uiObj.GetComponent<Selectable>() != null)
            return;
    }

    print("ボタンが押された!");
}

参考:Class Selectable | Unity UI | 1.0.0

参考:Method RaycastAll | Unity UI | 1.0.0

ただ、この判定処理だけではScrollRectのドラッグ操作など一部対応できないものも存在するため、必要に応じてタグ名などのチェック処理を追加すると良いでしょう。

注意

タグ名による判定はあくまでも一例です。これ以外にも、コンポーネントで判断したりすることも可能です。

開発するゲームやアプリケーションに合わせて適切に設計してください。

UI全体を対象とした場合の実装例

サンプルスクリプト

Imageなども含むUI要素全てを対象に、UI上にカーソルがあれば入力を受け付けなくする例です。

UIExclusiveExample.cs
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;

public class UIExclusiveExample : MonoBehaviour
{
    // 対象のAction
    [SerializeField] private InputActionProperty _buttonAction;

    private void OnEnable()
    {
        // Input ActionのperformedコールバックにOnButtonActionを登録
        _buttonAction.action.performed += OnButtonAction;
        _buttonAction.action.Enable();
    }

    private void OnDisable()
    {
        // Input ActionのperformedコールバックからOnButtonActionを削除
        _buttonAction.action.performed -= OnButtonAction;
        _buttonAction.action.Disable();
    }

    // Input Actionのperformedコールバック
    private void OnButtonAction(InputAction.CallbackContext context)
    {
        // UIの上にカーソルがあったら、入力を受け付けない
        if (EventSystem.current.IsPointerOverGameObject())
            return;

        print("ボタンが押された!");
    }
}

上記をUIExclusiveExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりInput Actionの入力情報の設定を行うと機能するようになります。

例では、Pointer > Press(Control Pathは<pointer>/press)を入力の受け取り対象として設定するものとします。

Tips

Control Pathに「Pointer」を指定すると、マウス(Mouse)、タッチ(Touch)、ペン(Pen)全ての操作を対象とすることができます。

参考:Pointers | Input System | 1.7.0

もしマウス入力だけにしたい場合は、Mouse > Left Buttonとしてください。

実行結果

マウスクリックした時、UI上にカーソルが無い状態では反応しカーソルがある状態では反応しません。

すべてのUIを対象とするため、Imageコンポーネント上で押された時も反応しません。

スクリプトの説明

カーソルがUIの上にあるかどうかの判定は、以下処理で行なっています。

// UIの上にカーソルがあったら、入力を受け付けない
if (EventSystem.current.IsPointerOverGameObject())
    return;

EventSystem.currentプロパティ現在のEvent Systemインスタンスを取得し、EventSystem.IsPointerOverGameObjectメソッドカーソルがEvent System管理下のゲームオブジェクト上に位置しているかどうかを取得します。

参考:Property current | Unity UI | 1.0.0

参考:Method IsPointerOverGameObject | Unity UI | 1.0.0

コールバックでUI上にカーソルがあるかどうかを判定し、カーソルがある時だけ入力を受け付けるまでの流れは以下のようになります。

// Input Actionのperformedコールバック
private void OnButtonAction(InputAction.CallbackContext context)
{
    // UIの上にカーソルがあったら、入力を受け付けない
    if (EventSystem.current.IsPointerOverGameObject())
        return;

    print("ボタンが押された!");
}

操作可能なUIに限定した場合の実装例

サンプルスクリプト

SelectableなUIのみを対象に、UI上にカーソルがあれば入力を受け付けなくする例です。

ScrollRectなどにも対応させるため、入力ブロックするタグで判定する処理も挟んでいます。

SelectableExclusiveExample.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;

public class SelectableExclusiveExample : MonoBehaviour
{
    // 対象のAction
    [SerializeField] private InputActionProperty _buttonAction;

    // カーソル位置取得用のAction
    [SerializeField] private InputActionProperty _cursorPositionAction;

    // Selectable以外の対象UIのタグ
    [SerializeField] private string _uiTag = "InputExclusiveUI";

    private PointerEventData _pointer;
    private readonly List<RaycastResult> _results = new();

    private void OnEnable()
    {
        // Input ActionのperformedコールバックにOnButtonActionを登録
        _buttonAction.action.performed += OnButtonAction;

        _buttonAction.action.Enable();
        _cursorPositionAction.action.Enable();
    }

    private void OnDisable()
    {
        // Input ActionのperformedコールバックからOnButtonActionを削除
        _buttonAction.action.performed -= OnButtonAction;

        _buttonAction.action.Disable();
        _cursorPositionAction.action.Disable();
    }

    // Input Actionのperformedコールバック
    private void OnButtonAction(InputAction.CallbackContext context)
    {
        // レイキャスト用の情報を作成
        _pointer ??= new PointerEventData(EventSystem.current);
        _pointer.position = _cursorPositionAction.action.ReadValue<Vector2>();

        // カーソル上にあるUIをレイキャストで取得
        _results.Clear();
        EventSystem.current.RaycastAll(_pointer, _results);

        // SelectableなUI上にカーソルがあったら、入力を受け付けない
        for (var i = 0; i < _results.Count; i++)
        {
            var uiObj = _results[i].gameObject;

            if (uiObj.CompareTag(_uiTag))
                return;

            if (uiObj.GetComponent<Selectable>() != null)
                return;
        }

        print("ボタンが押された!");
    }
}

上記をSelectableExclusiveExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターより各種Actionやタグ名の設定を行います。

Button ActionにUI操作との競合を防ぎたい対象のAction、Cursor Position Actionにカーソル位置を受け取るActionを設定してください。

また、必要に応じてUi Tagに操作対象とみなすUIのタグを指定します。

例では、Scroll Viewのスクロール領域が非SelectableなUIのため、Scroll View配下のContentオブジェクトに「InputExclusiveUI」タグを指定し、インスペクターにも「InputExclusiveUI」を指定するものとします。

実行結果

操作可能なUI(ボタン、スクロールビューなど)のみ入力がブロックされ、Imageなどの操作対象でないUIではブロックされず反応するようになりました。

スクリプトの説明

指定したカーソル位置でレイキャスト判定する必要があるため、次のようにカーソル位置を受け取るActionが必要です。

// 対象のAction
[SerializeField] private InputActionProperty _buttonAction;

// カーソル位置取得用のAction
[SerializeField] private InputActionProperty _cursorPositionAction;

また、レイキャスト判定の入出力用の変数を用意しておきます。

private PointerEventData _pointer;
private readonly List<RaycastResult> _results = new();

レイキャスト用の情報は、コールバックの先頭の以下処理で行います。

// Input Actionのperformedコールバック
private void OnButtonAction(InputAction.CallbackContext context)
{
    // レイキャスト用の情報を作成
    _pointer ??= new PointerEventData(EventSystem.current);
    _pointer.position = _cursorPositionAction.action.ReadValue<Vector2>();

_pointerがnullなら(初回なら)、新しいPointerEventDataインスタンスを生成してフィールドとして保持しておきます。

そして、PointerEventData.positionフィールドにカーソル位置用のActionからカーソル位置(スクリーン座標)を読んでセットしています。

作成されたPointerEventDataの情報に基づいて、レイキャストを行う処理は以下部分です。

// カーソル上にあるUIをレイキャストで取得
_results.Clear();
EventSystem.current.RaycastAll(_pointer, _results);

これにより、_results変数にヒットしたUIオブジェクトの一覧が格納されます。

参考:Method RaycastAll | Unity UI | 1.0.0

ヒットしたオブジェクトに対して入力のブロック対象かどうかを判定する処理は以下部分です。

// SelectableなUI上にカーソルがあったら、入力を受け付けない
for (var i = 0; i < _results.Count; i++)
{
    var uiObj = _results[i].gameObject;

    if (uiObj.CompareTag(_uiTag))
        return;

    if (uiObj.GetComponent<Selectable>() != null)
        return;
}

指定したブロック対象のタグ名である、またはSelectableなコンポーネントがあれば入力ブロックするUIとみなします。

Input Fieldへの対応

ここまでは、マウスなどによるクリック操作を主な対象として解説しました。

しかし、Input Fieldでのテキスト入力では、「選択されている時」にキーボードなどの入力をブロックする必要があります。

サンプルスクリプト

Input Field(TextMesh Proまたはレガシー版)が選択中に指定されたActionの入力をブロックする例です。

InputFieldExample.cs
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using TMPro;

public class InputFieldExample : MonoBehaviour
{
    // キー入力Action
    [SerializeField] private InputActionProperty _keyAction;

    private void OnEnable()
    {
        _keyAction.action.performed += OnKeyAction;
        _keyAction.action.Enable();
    }

    private void OnDisable()
    {
        _keyAction.action.performed -= OnKeyAction;
        _keyAction.action.Disable();
    }

    private void OnKeyAction(InputAction.CallbackContext context)
    {
        // 選択中のUI取得
        var selectedGameObject = EventSystem.current.currentSelectedGameObject;

        if (selectedGameObject != null)
        {
            // Input Fieldがフォーカス中なら何もしない
            if (
                selectedGameObject.GetComponent<TMP_InputField>() != null ||
                selectedGameObject.GetComponent<InputField>() != null
            )
            {
                return;
            }
        }

        print("キーが押された!");
    }
}

上記をInputFieldExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターからActionを設定すると機能するようになります。

ここでは、キーボードの「A」キーを入力として受け取るものとします。

実行結果

Input Fieldにフォーカスしている時は、入力が反応せずにログ出力されなくなります。

スクリプトの説明

Input Fieldが選択中なら入力をブロックする処理は以下部分です。

// 選択中のUI取得
var selectedGameObject = EventSystem.current.currentSelectedGameObject;

if (selectedGameObject != null)
{
    // Input Fieldがフォーカス中なら何もしない
    if (
        selectedGameObject.GetComponent<TMP_InputField>() != null ||
        selectedGameObject.GetComponent<InputField>() != null
    )
    {
        return;
    }
}

選択中のオブジェクトはEventSystem.currentSelectedGameObjectプロパティから取得できます。

参考:Property currentSelectedGameObject | Unity UI | 1.0.0

TextMesh Pro版とレガシー版でInput Fieldのクラスが異なるため、これらのどちらのクラスかをOR条件で調べています。 [1]

TextMesh Pro版TMP_InputFieldクラスレガシー版InputFieldクラスとなります。

参考:Class TMP_InputField | TextMeshPro | 3.0.6

参考:Class InputField | Unity UI | 1.0.0

アプリのフォーカス復帰時に入力を受け付けない

ここまで解説した例では、アプリケーションのフォーカスを一度失って復帰した時に、UIをクリックしたにも関わらずスルーせず反応してしまうことがあります。

これを回避するためには、アプリケーションのフォーカス復帰直後は入力をブロックするようにすれば良いです。

フォーカス復帰のタイミングは、MonoBehaviourのOnApplicationFocusイベントで取得できます。

// フォーカス処理
private void OnApplicationFocus(bool hasFocus)
{
    // フォーカス復帰時はhasFocusがtrueとなる
    if (hasFocus) {
        // 復帰時のフレームカウントを変数などに格納しておく
        _lastFocusFrameCount = Time.frameCount;
    }
}

サンプルスクリプト

以下、フォーカス復帰時の考慮も取り入れた例です。

SelectableなオブジェクトかどうかでUI判定する処理も含まれています。

CheckFocusExample.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;

public class CheckFocusExample : MonoBehaviour
{
    // 対象のAction
    [SerializeField] private InputActionProperty _buttonAction;

    // カーソル位置取得用のAction
    [SerializeField] private InputActionProperty _cursorPositionAction;

    // Selectable以外の対象UIのタグ
    [SerializeField] private string _uiTag = "InputExclusiveUI";

    // レイキャスト関連の入出力データ
    private PointerEventData _pointer;
    private readonly List<RaycastResult> _results = new();

    // アプリケーションのフォーカス復帰時のフレームカウント
    private int _lastFocusFrameCount;

    private void OnEnable()
    {
        // Input ActionのperformedコールバックにOnButtonActionを登録
        _buttonAction.action.performed += OnButtonAction;

        _buttonAction.action.Enable();
        _cursorPositionAction.action.Enable();
    }

    private void OnDisable()
    {
        // Input ActionのperformedコールバックからOnButtonActionを削除
        _buttonAction.action.performed -= OnButtonAction;

        _buttonAction.action.Disable();
        _cursorPositionAction.action.Disable();
    }

    // フォーカス処理
    private void OnApplicationFocus(bool hasFocus)
    {
        // フォーカス復帰時はhasFocusがtrueとなる
        if (hasFocus)
        {
            // 復帰時のフレームカウントを変数などに格納しておく
            _lastFocusFrameCount = Time.frameCount;
        }
    }

    // Input Actionのperformedコールバック
    private void OnButtonAction(InputAction.CallbackContext context)
    {
        // フォーカス復帰直後は入力を受け付けない
        if (Time.frameCount <= _lastFocusFrameCount + 1)
            return;

        // レイキャスト用の情報を作成
        _pointer ??= new PointerEventData(EventSystem.current);
        _pointer.position = _cursorPositionAction.action.ReadValue<Vector2>();

        // カーソル上にあるUIをレイキャストで取得
        _results.Clear();
        EventSystem.current.RaycastAll(_pointer, _results);

        // SelectableなUI上にカーソルがあったら、入力を受け付けない
        for (var i = 0; i < _results.Count; i++)
        {
            var uiObj = _results[i].gameObject;

            if (uiObj.CompareTag(_uiTag))
                return;

            if (uiObj.GetComponent<Selectable>() != null)
                return;
        }

        print("ボタンが押された!");
    }
}

上記をCheckFocusExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターよりActionの設定を行います。

設定内容は2つ目の例と一緒のため割愛します。

実行結果

フォーカスを失った状態から画面をクリックしてフォーカス復帰した時は、ボタンが反応しないようになりました。

フォーカス中は通常通りボタンが反応します。

スクリプトの説明

まず、フォーカス復帰直後かどうかを判定するために、復帰直後のフレームカウントを格納するフィールドを用意しておきます。

// アプリケーションのフォーカス復帰時のフレームカウント
private int _lastFocusFrameCount;

そして、フォーカス復帰時にこのフレームカウントを更新します。

// フォーカス処理
private void OnApplicationFocus(bool hasFocus)
{
    // フォーカス復帰時はhasFocusがtrueとなる
    if (hasFocus)
    {
        // 復帰時のフレームカウントを変数などに格納しておく
        _lastFocusFrameCount = Time.frameCount;
    }
}

ボタン入力のコールバックでは、フォーカス復帰より後かどうかの条件判定が加わっています。

これは以下部分です。

// フォーカス復帰直後は入力を受け付けない
if (Time.frameCount <= _lastFocusFrameCount + 1)
    return;

直近のフォーカス復帰時のフレームカウント以前のフレームなら、入力をブロックしています。

さいごに

マウス操作とUI操作の競合回避は、入力を読み込む時にUI上にカーソルがあるかどうかをチェックすることで実現できます。

キーボード入力とInput Field入力では、Input Fieldが選択中の時に入力をブロックするようにすれば良いです。

これらの処理を共通化したい場合、このようなマウスオーバー判定処理などをユーティリティクラスでラップするのが一つの解決策と言えるでしょう。

参考サイト

スポンサーリンク