【Unity】Input Systemでクリック/タップされた座標を取得する

こじゃらこじゃら

マウスカーソルがクリックされたら、その位置を取得するようなコードを書くにはどうすればいいの?

このはこのは

いくつか方法はあるけど、クリックされた瞬間などにコールバック側からマウス位置を取得する処理を書けば良いわ。

Input Systemでマウスカーソルのクリック位置や、画面タップされた位置などを取得する方法の解説記事です。

方法は一通りではありませんが、次の方法があります。

クリック位置を取得する方法
  • クリックされた瞬間にポインタ位置を取得する
  • カスタムComposie Bindingを実装する

1つ目の方法は、Input Systemを拡張せずに手軽に実現できる基本的な方法ですが、いくつか注意点もあります。

2つ目の方法は、Input Systemを拡張する必要がありますが、受け取り側のコードがシンプルになるメリットがあります。

いずれもタップやマルチタップ、長押しなどの操作にも対応しています。

本記事では、上記の2種類の方法でクリックまたはタップされた位置を取得する方法を解説していきます。

メモ

本記事では、「ポインタの位置」という言葉が出てきますが、マウスカーソルの位置やタッチパネルでタッチされた指の位置などを総称したものを意味します。

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

スポンサーリンク

前提条件

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

また、本記事のサンプルではInput Actionを使用します。

ここまでの基本的な解説は以下記事をご覧ください。

クリックされた瞬間にポインタ位置を取得する

クリックまたはタップされたときに、コールバックからポインタの位置を受け取る方法です。

ポインタの押下状態を受け取るActionを1つ用意し、クリックまたはタップされたときにポインタ位置を取得するコードを書くことで、現在のポインタ位置を取得できます。

InputAction pressAction;

・・・(中略)・・・

private void PressAction(InputAction.CallbackContext context)
{
    // デバイスを取得
    var pointer = Pointer.current;
    if (pointer == null)
        return;

    // クリック位置を取得
    var position = pointer.position.ReadValue();

    // 何か処理する
}

この方法には、次のメリットとデメリットがあります。

メリット・デメリット
  • メリット
    • Input System側の機能を拡張せずに実現できる
    • Updateイベント内での受け取りでも動作する
  • デメリット
    • 受取側のコードがやや複雑になる
    • コールバック内でポインタ位置をAction経由で受け取ったとき、フォーカス復帰時に正しく位置を取得できないことがある

最後のデメリットの理由については後述します。

Actionの設定

マウス左ボタンや指の押下状態などのボタン入力を受け取るActionを定義します。

Actionを次のように設定します。

  • Action TypeButton
  • Binding<Pointer>/press

Actionの設定方法の基本については、以下で解説しています。

Tips

MouseやTouchscreenではなくPointerを指定すると、マウスやタッチディスプレイにまとめて対応できるようになります。

Pointerはこれらを抽象化したデバイスであるためです。

参考:Pointers | Input System | 1.7.0

また、必要に応じてInteractionを設定します。

次のようなInteractionが設定できます。

Interaction一覧
  • Press – 押した瞬間、離した瞬間、その両方を検知する
  • Hold – 長押しされた瞬間を検知する
  • Tap – ボタンを押してすぐに離した瞬間を検知する
  • Multi Tap – 指定回数素早くタップされた瞬間を検知する
  • Slow Tap – ボタンをゆっくり押して離した瞬間を検知する

参考:Interactions | Input System | 1.7.0

サンプルスクリプト

以下、指定されたActionがトリガー [1] された瞬間にポインタの位置をログ出力する例です。

PressPositionExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class PressPositionExample : MonoBehaviour
{
    // ボタンの押下状態
    [SerializeField] private InputActionProperty _pressAction;
    
    private void Awake() => _pressAction.action.performed += PressAction;
    private void OnDestroy() => _pressAction.action.performed -= PressAction;
    private void OnEnable() => _pressAction.action.Enable();
    private void OnDisable() => _pressAction.action.Disable();

    private void PressAction(InputAction.CallbackContext context)
    {
        // Actionを使用すると、フォーカス復帰時に正しく位置取得できないことがあるので、
        // Pointerデバイスから直接位置を取得する
        var pointer = Pointer.current;
        if (pointer == null)
            return;

        // クリック位置を取得
        var position = pointer.position.ReadValue();
        
        // 取得座標をログ出力
        print($"Press position: {position}");
    }
}

上記をPressPositionExample.csという名前でUnityプロジェクトに保存し、適当なゲームオブジェクトにアタッチし、インスペクターより押し込み入力のActionを設定してください。

本記事では、前述のInput Action Assetで定義したActionを指定することとします。

実行結果

ゲーム画面をクリックすると、コンソールログにカーソルのスクリーン座標が出力されます。

画面外をクリック・タップしたときは反応しません。

スクリプトの説明

ボタンの押下状態を取得するActionは、以下のようにInputActionProperty型フィールドとして定義しています。

// ボタンの押下状態
[SerializeField] private InputActionProperty _pressAction;

InputAction型InputActionReference型でも良いですが、InputActionProperty型とすることで両者に対応できます。

参考:Struct InputActionProperty | Input System | 1.7.0

コールバックの登録はAwake、解除はDestroyのタイミングで行っています。

private void Awake() => _pressAction.action.performed += PressAction;
private void OnDestroy() => _pressAction.action.performed -= PressAction;

このままではActionが有効にならず入力を受け取れないため、InputAction.Enableメソッドで有効化する必要があります。

本記事では、コンポーネントの有効化・無効化のタイミングでActionの有効化・無効化を制御しています。

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

ボタン入力を受け取るコールバック内の処理では、Pointerクラスに直接アクセスして入力を受け取っています。

private void PressAction(InputAction.CallbackContext context)
{
    // Actionを使用すると、フォーカス復帰時に正しく位置取得できないことがあるので、
    // Pointerデバイスから直接位置を取得する
    var pointer = Pointer.current;
    if (pointer == null)
        return;

    // クリック位置を取得
    var position = pointer.position.ReadValue();
    
    // 取得座標をログ出力
    print($"Press position: {position}");
}

Pointer.currentプロパティを取得し、これがnullでなければデバイスが存在しているので、positionプロパティReadValueメソッドからクリック位置を取得しています。

参考:Class Pointer | Input System | 1.7.0

位置をActionで受け取る場合の問題点

ポインタ位置をPointerクラスではなくAction経由で受け取ることも可能です。

しかし、この方法には次の問題があります。

  • コールバックから別のActionを呼び出す場合、コールバック順序によって入力値が変わることがある
  • 特にフォーカス復帰時では、順序の問題によりポインタ位置が(0, 0)となることがある

以下は、その問題のあるコードです。

PressPositionExampleWithProblem.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class PressPositionExampleWithProblem : MonoBehaviour
{
    // ボタンの押下状態
    [SerializeField] private InputActionProperty _pressAction;

    // ポインタの位置
    [SerializeField] private InputActionProperty _positionAction;

    private void Awake() => _pressAction.action.performed += PressAction;
    private void OnDestroy() => _pressAction.action.performed -= PressAction;
    private void OnEnable()
    {
        _pressAction.action.Enable();
        _positionAction.action.Enable();
    }

    private void OnDisable()
    {
        _pressAction.action.Disable();
        _positionAction.action.Disable();
    }

    private void PressAction(InputAction.CallbackContext context)
    {
        // クリック位置を取得
        var position = _positionAction.action.ReadValue<Vector2>();

        // 取得座標をログ出力
        print($"Press position: {position}");
    }
}

このスクリプトでは、次のようにフォーカス復帰した瞬間だけカーソル位置が(0, 0)になります。

特に、Input System Package > Background Behaviour項目Ignore Focusになっていないと、フォーカスを失った瞬間にデバイスが無効化されるため、復帰した瞬間にポインタ位置を返すActionが先に動作しないとこのような現象が発生してしまいます。

メモ

Input Actionは内部的にはInteractionと呼ばれる一種のステートマシンを持っています。入力が無い場合はWaiting、入力され始めにStarted、特定入力があったときなど(ボタンが一定の深さ以上押し込まれる等)にPerformed、入力が終わるとCanceledの状態(フェーズ)に遷移します。

また、Input Actionが無効化された場合はDisabled状態になります。

このうち、InputAction.ReadValueメソッドが(0, 0)、すなわち初期値以外の値を返すためにはInteractionのフェーズがStartedまたはPerformedになっている必要があります。

カスタムComposie Bindingを実装する

カスタムComposite Bindingを実装してクリックされたときにポインタ位置を取得することも可能です。

これは、ボタンが押されたときにポインタ位置を返す独自のComposite Bindingを自作して適用することで解決します。

Composite Bindingの構成

クリックされたときなどにポインタ位置を返す挙動ですが、Composite Bindingが返す値は常にPosition(ポインタ位置)返す大きさは常にPressの大きさとします。

このようにすることで、ボタンの離された瞬間に反応するInteractionでもポインタ位置が受け取れるようになります。

注意

似たようなComposite BindingにOne Modifierがありますが、こちらは前述のPositionに対する大きさを返す挙動になっているので、離された瞬間にトリガーするInteractionでは座標(0, 0)を返す挙動になってしまいます。

また、スクリーン座標の原点(0, 0)をクリック・タップしたときも入力の大きさが0であるため、入力なしと判断されてしまいます。

これを回避するために、本記事ではボタンを離した時でも(0, 0)ではなく本来のポインタ座標を返すComposite Bindingを実装する方法を提案しています。

この方法には、次のメリットとデメリットがあります。

メリット・デメリット
  • メリット
    • 受け取り側のコードをシンプルにできる
    • デバイス依存しないコードが書ける
    • フォーカス復帰処理にも対応可能
  • デメリット
    • Composite Bindingを独自実装する必要がある
    • Updateイベント内などでは使用できない(コールバック限定)

したがって、コールバック内限定でクリック位置を取得したいケースでは、こちらの方法の方がより適切と言えるでしょう。

カスタムComposite Bindingの実装例

以下、指定されたボタン入力があったときにポインタ位置を返すComposite Bindingの実装例です。

ボタン入力とポインタ位置はそれぞれActionとして指定可能になっています。

PressPositionComposite.cs
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.Utilities;

public class PressPositionComposite : InputBindingComposite<Vector2>
{
    // ボタンの押下状態
    [InputControl(layout = "Button")] public int press;

    // ポインタの位置
    [InputControl(layout = "Vector2")] public int position;

    /// <summary>
    /// 初期化
    /// </summary>
#if UNITY_EDITOR
    [UnityEditor.InitializeOnLoadMethod]
#else
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif
    private static void Initialize()
    {
        // 初回にCompositeBindingを登録する必要がある
        InputSystem.RegisterBindingComposite<PressPositionComposite>("PressPosition");
    }

    public override Vector2 ReadValue(ref InputBindingCompositeContext context)
    {
        // ポインタの位置を返す
        return context.ReadValue<Vector2, Vector2MagnitudeComparer>(position);
    }

    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        // ボタンの押下状態を大きさとして返す
        return context.ReadValue<float>(press);
    }
}

上記をPressPositionComposite.csという名前でUnityプロジェクトに保存すると、カスタムComposite Bindingとして使用可能になります。

適用方法

クリックやタップされたらポインタの位置を取得するようにActionを設定します。

本記事では、新しくポインタ位置を返すActionを定義するものとします。Action TypeValueControl TypeVector 2とします。 [2]

次に、どの瞬間に反応(トリガー)するかをInteractionsから指定します。例では、押してすぐ離す操作を表す「Tap」を指定するものとします。この辺はお好みで設定してください。

次に、前述の自作のComposite BindingをActionに追加します。

Action作成時に自動作成されるBindingを削除し、Action右の「+」アイコンからAdd Press Position Composite [3] を選択し、カスタムComposite Bindingを追加します。

そして、Pressポインタの押し込み入力などのBinding、Positionポインタ位置などのBindingをそれぞれ指定します。

取得側のスクリプトの例(必要ならば)

前述のCompoisite Bindingを適用したActionの入力を受け取る処理を実装します。本記事では、次の新しいスクリプトで入力を受け取るものとします。

CompositeExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class CompositeExample : MonoBehaviour
{
    // ポインタの位置を受け取るAction
    [SerializeField] private InputActionProperty _pressPositionAction;

    private void Awake() => _pressPositionAction.action.performed += OnPress;
    private void OnDestroy() => _pressPositionAction.action.performed -= OnPress;

    private void OnEnable() => _pressPositionAction.action.Enable();
    private void OnDisable() => _pressPositionAction.action.Disable();
    
    private void OnPress(InputAction.CallbackContext context)
    {
        // クリック位置を取得
        var position = _pressPositionAction.action.ReadValue<Vector2>();
        
        // 取得座標をログ出力
        print($"Press position: {position}");
    }
}

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

実行結果

フォーカス復帰した瞬間も正しくクリック位置が取得できていることが確認できます。

スクリプトの説明

ボタンが押されたときだけポインタ位置を返すカスタムComposite Bindingは、次のように実装しています。

public class PressPositionComposite : InputBindingComposite<Vector2>

InputBindingComposite<T>クラスを継承する形で行います。

参考:Class InputBindingComposite<TValue> | Input System | 1.7.0

Composite Bindingでは、ボタン入力とポインタ位置の2つのBindingを入力とするため、次のようにint型フィールドを定義しています。

// ボタンの押下状態
[InputControl(layout = "Button")] public int press;

// ポインタの位置
[InputControl(layout = "Vector2")] public int position;

フィールドの型はint型ですが、[InputControl]属性を付加しています。これによってBindingとして識別可能になります。

例では、layoutプロパティでBindingの入力型を制限しています。

参考:Class InputControl | Input System | 1.7.0

Composite Bindingが出力する値は、ポインタ位置そのものです。

public override Vector2 ReadValue(ref InputBindingCompositeContext context)
{
    // ポインタの位置を返す
    return context.ReadValue<Vector2, Vector2MagnitudeComparer>(position);
}

また、Composite Bindingが返す入力値の大きさは、次のようにボタン入力の大きさをそのまま返すようにしています。

public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
{
    // ボタンの押下状態を大きさとして返す
    return context.ReadValue<float>(press);
}

さいごに

マウスがクリックされたときの位置を取得するには、コールバックなどからカーソルや指の位置を取得する必要がありますが、2つのActionを使用する場合は注意が必要です。

コールバック内から別の位置取得用Actionを参照しても実現はできますが、フォーカス復帰時にActionが有効になっていない可能性があり、原点位置(0, 0)を返してしまう場合があります。

これを回避するためには、コールバック内ではPointer.current.positionなどのプロパティに直接アクセスするか、カスタムComposite Bindingを実装して適用するのが安全です。

関連記事

参考サイト

スポンサーリンク