【Unity】Input Systemでピンチやマルチスワイプを実現する

こじゃらこじゃら

Input Systemを使ってスマホのピンチマルチスワイプを実装するにはどうすればいいの?

このはこのは

2本指のタッチ入力を取得して判定すれば良いわ。

Input Systemを用いてタッチスクリーンのピンチインやピンチアウト、マルチスワイプなどの操作を判定する方法の解説記事です。

実装方法は一通りではありませんが、本記事ではInput Action経由で検知する方法を紹介します。

Input Actionを経由して操作入力を判定することにより、次のメリットが得られます。

メリット
  • 入力デバイスへのアクセスを抽象化できる
  • 操作判定ロジックとゲームロジックを綺麗に分離することができる

特に2つ目のメリットについては、複数入力の合成を実現するComposite Bindingを用いることで、更に大きな効果が見込めます。

本記事では、Input SystemのActionを経由してピンチインやピンチアウト、マルチスワイプ操作を実装する方法について解説します。また、後半ではカスタムComposite Bindingで実現する方法も紹介します。

動作環境
  • Unity 2022.1.20f1
  • Input System 1.4.4
  • iOS 16.0.3

スポンサーリンク

前提条件

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

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

また、本記事ではPlayer Input経由でInput Actionからピンチやマルチスワイプの操作を取得するものとして解説を進めていきます。

Input Actionの仕組みや使い方は以下記事で解説しています。

Player Inputの使い方は以下記事で解説しています。

本記事のサンプルスクリプトを動かすには、タッチスクリーンでプログラムを実行できる環境が必要になりますので、予めご了承ください。

全体的な実装の流れ

Input SystemのPlayer Input経由でピンチ・マルチスワイプ判定を行う実装は次のような流れになります。

実装の流れ
  • 2本指のタッチ入力を取得するActionを定義する
  • 2本指のタッチ入力を取得し、ピンチ・マルチスワイプ操作の判定を行うスクリプトを実装する
  • Player Inputで上記スクリプトにタッチ入力を渡す設定を行う

スクリプトの実装では、2つのタッチ情報から受け取り側で計算する方法カスタムComposite Bindingを実装して計算する方法などがあります。

本記事では、これらの方法について例を示しながら解説します。

Input Actionの定義

前準備として、2つのタッチ入力を受け取るInput Actionを定義します。

Composite Bindingを用いる方法は後述します。

Input Actionの設定

Input Actionアセットを用意します。

本記事では、新規にInput Actionアセットを作成するものとします。既に使用中のInput Actionアセットがあれば、こちらに追加する形でも問題ありません。

タッチ入力を取得するために、次のような設定のActionを2つ定義します。

  • Action TypeValue
  • Control TypeTouch

そして、追加した2つのActionに次のBindingを設定します。

  • Touch_0Touch #0 [Touchscreen](コントロールパス : <Touchscreen>/touch0
  • Touch_1Touch #1 [Touchscreen](コントロールパス : <Touchscreen>/touch1

メモ

上記ではControl TypeをTouchとしてタッチ入力を一気に取得するようにしていますが、タッチ位置や押下判定などの情報を別々のActionとして定義して取得することも可能です。

参考:Touch support | Input System | 1.4.4

ピンチ操作を検知する

前述のActionからタッチ入力を取得してピンチ判定を行うスクリプトを実装します。

次のように、2本指の距離変化を取得することとします。

サンプルスクリプト

ピンチ判定処理を実装した例です。

PinchExample.cs
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;

public class PinchExample : MonoBehaviour
{
    // 2本指のタッチ情報
    private TouchState _touchState0;
    private TouchState _touchState1;
    
    // Touch #0 入力
    public void OnTouch0(InputAction.CallbackContext context)
    {
        _touchState0 = context.ReadValue<TouchState>();

        OnPinch();
    }
    
    // Touch #1 入力
    public void OnTouch1(InputAction.CallbackContext context)
    {
        _touchState1 = context.ReadValue<TouchState>();

        OnPinch();
    }

    // ピンチ判定処理
    private void OnPinch()
    {
        // 2本指が移動していなかれば操作なしと判断
        if (!_touchState0.isInProgress || !_touchState1.isInProgress) return;

        // タッチ位置(スクリーン座標)
        var pos0 = _touchState0.position;
        var pos1 = _touchState1.position;

        // 移動量(スクリーン座標)
        var delta0 = _touchState0.delta;
        var delta1 = _touchState1.delta;
        
        // 移動前の位置(スクリーン座標)
        var prevPos0 = pos0 - delta0;
        var prevPos1 = pos1 - delta1;

        // 距離の変化量を求める
        var pinchDelta = Vector3.Distance(pos0, pos1) - Vector3.Distance(prevPos0, prevPos1);
        
        // 距離の変化量をログ出力
        Debug.Log($"ピンチ操作量 : {pinchDelta}");
    }
}

Player InputからUnityEvent経由でタッチ入力を受け取る想定のスクリプトです。

Player Inputの設定

前述のスクリプトにInput Systemのタッチ情報を渡すための設定をPlayer Inputより行います。

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

前述のサンプルスクリプトを適当なゲームオブジェクトにアタッチします。例ではPlayer Inputと同じゲームオブジェクトにアタッチするものとします。

Player InputコンポーネントのAction項目に前の手順で作成したInput Actionアセットを指定します。

Behaviour項目Invoke Unity Eventsを設定し、各タッチActionにOnTouch0、OnTouch1メソッドを登録します。

実行結果

2本指でピンチインするとマイナス値がログ出力され、ピンチアウトするとプラス値がログされるようになりました。

なお、上記動画では、デバッグログの内容をUIのテキスト側に表示させるようにしています。実行ログが見れる環境であれば何でも構いません。 [1]

スクリプトの説明

タッチ入力はTouchState構造体として受け取ります。TouchState構造体はUnityEngine.InputSystem.LowLevel名前空間に属するため、次のようにusingします。

using UnityEngine.InputSystem.LowLevel;

タッチ入力情報は、次のように取得できます。

_touchState0 = context.ReadValue<TouchState>();

TouchState構造体には、タッチ位置や移動量、画面に指が触れられているかどうかなど、様々な情報が格納されています。

構造体の仕様については、以下リファレンスのページをご覧ください。

参考:Struct TouchState | Input System | 1.4.4

ピンチ処理では、まず2本指がスワイプされているかを判定するため、TouchState.isInProgressフィールドをチェックしています。

// 2本指が画面に触れていなかれば操作なしと判断
if (!_touchState0.isInProgress || !_touchState1.isInProgress) return;

ピンチ判定を行うためには、現在位置移動前の位置が必要になるため、次のコードで移動前の位置を計算します。

// タッチ位置(スクリーン座標)
var pos0 = _touchState0.position;
var pos1 = _touchState1.position;

// 移動量(スクリーン座標)
var delta0 = _touchState0.delta;
var delta1 = _touchState1.delta;

// 移動前の位置(スクリーン座標)
var prevPos0 = pos0 - delta0;
var prevPos1 = pos1 - delta1;

ピンチイン、ピンチアウトの操作量は、タッチ位置の距離の変化量から算出します。

// 距離の変化量を求める
var pinchDelta = Vector3.Distance(pos0, pos1) - Vector3.Distance(prevPos0, prevPos1);

マルチスワイプを検知する

本記事では、2本指の中点の移動量を取得するものとします。

サンプルスクリプト

マルチスワイプ操作を判定する例です。

MultiSwipeExample.cs
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;

public class MultiSwipeExample : MonoBehaviour
{
    // 2本指のタッチ情報
    private TouchState _touchState0;
    private TouchState _touchState1;

    // Touch #0 入力
    public void OnTouch0(InputAction.CallbackContext context)
    {
        _touchState0 = context.ReadValue<TouchState>();

        OnMultiSwipe();
    }

    // Touch #1 入力
    public void OnTouch1(InputAction.CallbackContext context)
    {
        _touchState1 = context.ReadValue<TouchState>();

        OnMultiSwipe();
    }

    // ピンチ判定処理
    private void OnMultiSwipe()
    {
        // 2本指が移動していなかれば操作なしと判断
        if (!_touchState0.isInProgress || !_touchState1.isInProgress) return;

        // 移動量(スクリーン座標)
        var delta0 = _touchState0.delta;
        var delta1 = _touchState1.delta;

        // 中点の変化量を求める
        var multiSwipeDelta = (delta0 + delta1) / 2;

        // ログ出力
        Debug.Log($"Multi swipe delta : {multiSwipeDelta}");
    }
}

Player Inputへの設定方法はピンチ操作の例と一緒のため割愛させていただきます。

実行結果

マルチスワイプされたときに、中点の移動量がベクトルとして得られるようになりました。

スクリプトの説明

中点の移動量を求める処理は以下部分です。

// 中点の変化量を求める
var multiSwipeDelta = (delta0 + delta1) / 2;

2本指の移動量の相加平均から計算できます。

Composite Bindingで実装する

ここまで紹介した方法は、入力の受け取り側で操作量を計算していました。

このような計算ロジックは、Input SystemのComposite Binding側で実装することも可能です。

Composite Bindingとは、複数入力をまとめたり、入力型を変換したりする機能を有するBindingです。

参考:Input Bindings | Input System | 1.4.4

例えば、ピンチ操作2つのタッチ情報(TouchState型)から距離の変化量(float型)に変換して受け取り側に値を渡すことが可能になります。

これにより、入力ロジックとゲームロジックで処理が綺麗に分かれるメリットが得られます。

2つのタッチ情報からピンチ・マルチスワイプの操作量に変換するComposite Bindingの実装例を紹介します。

注意

カスタムComposite Bindingを実装する際は、ステートレスである必要があることにご注意ください。例えば、Composite Binding側で内部的に変数を持ったりすることはできません。

参考:Input Bindings | Input System | 1.4.4

ピンチ操作をComposite Bindingで実装する

2つのタッチ情報からピンチ操作量に変換するComposite Bindingの実装例です。

PinchComposite.cs
using System.ComponentModel;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;

[DisplayName("Pinch Composite")]
public class PinchComposite : InputBindingComposite<float>
{
    // タッチ入力
    [InputControl(layout = "Touch")] public int touch0 = 0;
    [InputControl(layout = "Touch")] public int touch1 = 0;

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

    /// <summary>
    /// 値の大きさを返す
    /// </summary>
    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        return ReadValue(ref context);
    }

    /// <summary>
    /// ピンチ操作量の取得
    /// </summary>
    public override float ReadValue(ref InputBindingCompositeContext context)
    {
        var touchState0 = context.ReadValue<TouchState, TouchDeltaMagnitudeComparer>(touch0);
        var touchState1 = context.ReadValue<TouchState, TouchDeltaMagnitudeComparer>(touch1);

        // 2本指が移動していなかれば操作なしと判断
        if (!touchState0.isInProgress || !touchState1.isInProgress)
            return 0;

        // タッチ位置(スクリーン座標)
        var pos0 = touchState0.position;
        var pos1 = touchState1.position;

        // 移動量(スクリーン座標)
        var delta0 = touchState0.delta;
        var delta1 = touchState1.delta;

        // 移動前の位置(スクリーン座標)
        var prevPos0 = pos0 - delta0;
        var prevPos1 = pos1 - delta1;

        return Vector2.Distance(pos0, pos1) - Vector2.Distance(prevPos0, prevPos1);
    }
}
TouchDeltaMagnitudeComparer.cs
using System.Collections.Generic;
using UnityEngine.InputSystem.LowLevel;

public class TouchDeltaMagnitudeComparer : IComparer<TouchState>
{
    public int Compare(TouchState x, TouchState y)
    {
        var lenX = x.delta.sqrMagnitude;
        var lenY = y.delta.sqrMagnitude;

        if (lenX < lenY)
            return -1;
        if (lenX > lenY)
            return 1;
        return 0;
    }
}

サンプルスクリプトが2つあることにご注意ください。上記スクリプトをUnityプロジェクトに保存すると機能するようになります。

Input Actionの設定

ピンチ操作をfloat型として受け取るため、次の設定のActionを定義します。

  • Action TypeValue
  • Control TypeAxis

そして、Action配下に先ほどの例のPinch CompositeというComposite Bindingを追加し、Touch 0、Touch 1タッチ入力のBindingを指定してください。

入力受け取りスクリプトの実装

前述のピンチ操作を受け取るスクリプトを実装します。あくまで例のため、本手順は必須ではありません。

以下、入力を受け取ってログ出力する例です。

PinchCompositeExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class PinchCompositeExample : MonoBehaviour
{
    public void OnPinch(InputAction.CallbackContext context)
    {
        // ピンチ操作量取得
        var pinchDelta = context.ReadValue<float>();
        
        // 距離の変化量をログ出力
        Debug.Log($"Pinch delta : {pinchDelta}");
    }
}

例ではスクリプトを新しく作成していますが、既存のスクリプトにメソッドを実装する形でも問題ありません。

実装したメソッドで入力を受け取れるようにするため、Player InputのActionにメソッドを登録しておきます。

実行結果

一つ目の例と結果は一緒ですが、Composite Binding経由でピンチ操作量を取得できるようになりました。

スクリプトの説明

カスタムComposite Bindingを作成するために、InputBindingComposite継承クラスを定義します。

[DisplayName("Pinch Composite")]
public class PinchComposite : InputBindingComposite<float>

得られるピンチ操作量はfloat型にしたいため、InputBindingComposite<float>継承クラスとしています。

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

上記のままではInput Actionのメニューに表示されないため、次のようにComposite BindingをInput System側に登録する必要があります。

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

Unityエディタではエディタロード時に、ビルドではシーンロード前に登録処理を走らせるため、それぞれ#ifディレクティブで場合分けして属性を指定しています。

Composite Binding側が受け取る2本指のタッチ入力は、以下で定義しています。

// タッチ入力
[InputControl(layout = "Touch")] public int touch0 = 0;
[InputControl(layout = "Touch")] public int touch1 = 0;

InputControl属性引数layoutTouchを指定することで、TouchState型のBindingに制限できます。

参考:Class InputControlAttribute | Input System | 1.4.4

タッチ入力の受け取りは、以下部分で行っています。

/// <summary>
/// ピンチ操作量の取得
/// </summary>
public override float ReadValue(ref InputBindingCompositeContext context)
{
    var touchState0 = context.ReadValue<TouchState, TouchDeltaMagnitudeComparer>(touch0);
    var touchState1 = context.ReadValue<TouchState, TouchDeltaMagnitudeComparer>(touch1);

InputBindingCompositeContext.ReadValueメソッドでは入力値の大きさの比較が必要になるため、テンプレート引数の2つ目に独自の比較クラスTouchDeltaMagnitudeComparerを指定しています。 [2]

TouchDeltaMagnitudeComparerIComparer継承クラスで、TouchState.deltaの大きさで比較するようにしています。

public class TouchDeltaMagnitudeComparer : IComparer<TouchState>
{
    public int Compare(TouchState x, TouchState y)
    {
        var lenX = x.delta.sqrMagnitude;
        var lenY = y.delta.sqrMagnitude;

        if (lenX < lenY)
            return -1;
        if (lenX > lenY)
            return 1;
        return 0;
    }
}

マルチスワイプ操作をComposite Bindingで実装する

マルチスワイプ操作をComposite Bindingで実装する場合も、ピンチ操作と同じ要領でできます。

以下、実装例です。

MultiSwipeComposite.cs
using System.ComponentModel;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;

[DisplayName("Multi Swipe Composite")]
public class MultiSwipeComposite : InputBindingComposite<Vector2>
{
    // タッチ入力
    [InputControl(layout = "Touch")] public int touch0 = 0;
    [InputControl(layout = "Touch")] public int touch1 = 0;

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

    /// <summary>
    /// 値の大きさを返す
    /// </summary>
    public override float EvaluateMagnitude(ref InputBindingCompositeContext context)
    {
        return ReadValue(ref context).magnitude;
    }

    /// <summary>
    /// マルチスワイプ操作量の取得
    /// </summary>
    public override Vector2 ReadValue(ref InputBindingCompositeContext context)
    {
        var touchState0 = context.ReadValue<TouchState, TouchDeltaMagnitudeComparer>(touch0);
        var touchState1 = context.ReadValue<TouchState, TouchDeltaMagnitudeComparer>(touch1);
        
        // 2本指が移動していなかれば操作なしと判断
        if (!touchState0.isInProgress || !touchState1.isInProgress)
            return Vector2.zero;

        // 移動量(スクリーン座標)
        var delta0 = touchState0.delta;
        var delta1 = touchState1.delta;

        // 中点の変化量を求める
        var multiSwipeDelta = (delta0 + delta1) / 2;

        return multiSwipeDelta;
    }
}

スクリプト中で参照されているTouchDeltaMagnitudeComparerクラスは、先述のサンプルスクリプトで定義されたものを用いるものとします。

Input Actionの設定

マルチスワイプ操作量はVector2型として受け取るため、次のようなActionを定義します。

  • Action TypeValue
  • Control TypeVector 2

そして、Action配下にMulti Swipe Compositeを追加し、タッチ入力を定義してください。

入力受け取りスクリプトの実装

マルチスワイプ入力の受け取り側の実装例です。

MultiSwipeCompositeExample.cs
using UnityEngine;
using UnityEngine.InputSystem;

public class MultiSwipeCompositeExample : MonoBehaviour
{
    public void OnMultiSwipe(InputAction.CallbackContext context)
    {
        // マルチスワイプ操作量取得
        var multiSwipeDelta = context.ReadValue<Vector2>();
        
        // ログ出力
        Debug.Log($"Multi swipe delta : {multiSwipeDelta}");
    }
}

ピンチ操作の例と同様に、Player InputのActionにメソッドを登録しておきます。

実行結果

Composite Binding経由でマルチスワイプ動作が取得できていることを確認できました。

スクリプトの説明

Vector2型の移動量に変換するComposite Bindingを実装したいため、以下のようにInputBindingComposite<Vector2>クラスを継承しています。

[DisplayName("Multi Swipe Composite")]
public class MultiSwipeComposite : InputBindingComposite<Vector2>

旧バージョンによる不具合

当環境で検証した際、Input System 1.4.3以前ではComposite Binding経由でタッチスクリーン情報を取得すると、一部タッチ入力が正しく取得できない現象を確認しました。

Input System 1.4.4以降ではこの問題は解消されていることを確認済みです。

Changelog上では、Input System 1.4.1にてタッチスクリーンに関する不具合が1つ修正されているようです。

参考:Changelog | Input System | 1.4.4

基本的に最新バージョンにアップデートしておけば問題ないと思われます。

さいごに

Input Systemでピンチイン/ピンチアウトやマルチスワイプを実装する場合、Input Action経由で実現できます。

カスタムComposite Bindingでこの辺の処理を実装できれば、ゲーム側と入力側で綺麗にロジックを分けて管理でき便利です。

両方の操作を一緒に扱いたい場合、それぞれの操作入力を取得すればよいため再利用もしやすいでしょう。

関連記事

参考サイト

スポンサーリンク