【Unity】スマホのマルチタッチ(マルチタップ)の実装

# はじめに

スマホのマルチタッチを実装しようとした時に次のような問題に遭遇しました。

- 2本の指でタップすると、そのタップしている2点の中間の場所を取得してしまう。
- 複数の指をタップしたり画面から離したりすると、それぞれの指が干渉してしまい、予期せぬ動作をしてしまう。

この記事ではこれらの問題を解決し、スマホのマルチタッチ操作を実装するために必要な情報をまとめました。

# (筆者の)実行環境

- macOS Catalina 10.15.2
- Unity 2019.3.0f3

しかし、最新の機能やライブラリを使っているわけではないので、ある程度古いバージョンのUnityでも上手くいくはずです。

# 注意

本記事における実装は、スマホの実機でのみ動作します。エディタ上では動作しないのでご注意ください。

# 実装

## 使うもの

**[UnityEngine.Touch](https://docs.unity3d.com/ja/current/ScriptReference/Touch.html)**を使います。

Touchとは、スクリーンに触れている指の状態に関する情報を持つ構造体で、次のような情報を持ちます。

- 画面に触れている指の本数
- それぞれの指を識別するためのid
- それぞれの指の状態(タップした、動いている、止まっている、指を離した等)

なので、画面に触れている複数の指をそれぞれトラッキングすることができます。

Touchには他にも「指と画面の間に生じている圧力」などの情報も持つので、ここでは扱いませんが、もしかしたら「圧力」などを使った面白いことができるかもしれません。

## 基本となるコード

`Update()`内にコードを書きます。
<details><summary>コードを見る</summary><div>

```cs
void Update()
{
    var touchCount = Input.touchCount;
    
    for (var i = 0; i < touchCount; i++)
    {
        var touch = Input.GetTouch(i);
        switch (touch.phase)
        {
            case TouchPhase.Began:
                // 画面に指が触れた時に行いたい処理をここに書く
                break;
            case TouchPhase.Moved:
                // 画面上で指が動いたときに行いたい処理をここに書く
                break;
            case TouchPhase.Stationary:
                // 指が画面に触れているが動いてはいない時に行いたい処理をここに書く
                break;
            case TouchPhase.Ended:
                // 画面から指が離れた時に行いたい処理をここに書く
                break;
            case TouchPhase.Canceled:
                // システムがタッチの追跡をキャンセルした時に行いたい処理をここに書く
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}
```
</div></details>

### 解説

最初に、

```cs
var touchCount = Input.touchCount;
```
で画面に触れている指の数を取得します。

次に、forループを`touchCount`の数だけ回します。

そして、

```cs
var touch = Input.GetTouch(i);
```
により、画面に触れている指の情報が格納された**Touch構造体**のインスタンスを取得します。

最後に、`touch.Phase`で「タップしている指が今どのような状態なのか」を取得し、`switch`で処理を分岐させています。

# 応用例 (ボタンをタップして移動させる)

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/d04b70d5-fd22-ccbc-7598-876c7d63327a.gif)

まずButtonのuGUIを作成し、EventTriggerをAddComponentします。(Button、と書きましたが、EventTriggerを用いるためButtonコンポーネントは不要なのでRemoveしてしまって下さい。)

Buttonに次のMoveButtonコンポーネントをアタッチします。
<details><summary>MoveButtonのコードを見る</summary><div>

```cs

using System;
using UnityEngine;
using UnityEngine.EventSystems;

public class MoveButton : MonoBehaviour
{
    private RectTransform _rectTransform;
    private int _fingerId = -1;

    private void Awake()
    {   
        _rectTransform = GetComponent<RectTransform>();

        var eventTrigger = GetComponent<EventTrigger>();
        
        var pointerDownEntry = new EventTrigger.Entry {eventID = EventTriggerType.PointerDown};
        
        pointerDownEntry
            .callback
            .AddListener((data) => OnPointerDownDelegate((PointerEventData)data));
            
        eventTrigger
            .triggers
            .Add(pointerDownEntry);
    }

    private void Update()
    {
        var touchCount = Input.touchCount;
        
        if(touchCount <= 0)
            return;

        for (var i = 0; i < touchCount; i++)
        {
            var touch = Input.GetTouch(i);

            if (touch.fingerId == _fingerId)
            {
                switch (touch.phase)
                {
                    case TouchPhase.Began:
                    case TouchPhase.Moved:
                    case TouchPhase.Stationary:
                        _rectTransform.position = touch.position;
                        break;
                    case TouchPhase.Ended:
                        _fingerId = -1;
                        break;
                    case TouchPhase.Canceled:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }
        }
    }

    private void OnPointerDownDelegate(PointerEventData data)
    {
        var touchCount = Input.touchCount;

        switch (touchCount)
        {
            case 1:
            {
                var touch = Input.GetTouch(0);

                _fingerId = touch.fingerId;
                break;
            }
            case 2:
            {
                var touch0 = Input.GetTouch(0);
                var touch1 = Input.GetTouch(1);

                // タップしている2本の指のうち、適する方を選ぶ
                var delta0 = math.lengthsq(touch0.position - data.position);
                var delta1 = math.lengthsq(touch1.position - data.position);
                var touch = delta0 < delta1 ? touch0 : touch1;

                _fingerId = touch.fingerId;
                break;
            }
        }
    }
}
```
</div></details>

実機で実行してみると上のGIFのようになります。
実は実行中にButton以外の余計な場所を色々とタップしているのですが、特に干渉することなく普通に動いています。

2つのButtonをそれぞれ動かすこともできます。

![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164430/9a0bedb4-8d77-7be7-8135-0c06fd639199.gif)


### 解説

Unityではそれぞれの指をトラッキングするために、画面をタップした瞬間にそれぞれの指にidが振り分けられます。
[Touch-fingerId - Unity スクリプトリファレンス](https://docs.unity3d.com/ja/2018.4/ScriptReference/Touch-fingerId.html)

Buttonをタップした指をそれ以外の指と区別する必要があるので、Buttonをタップした指のidを`_fingerId`で管理することにしています。
指に振り分けられるidは0以上の整数なので、使われる可能性が無い`-1`で`_fingerId`を初期化しています。

`Awake()`内でButtonをタップした瞬間に実行される`OnPointerDownDelegate()`関数を登録します。

```cs

var touchCount = Input.touchCount;
```
により画面にタップしている指の本数を取得し、その値により処理を分岐させ、`_fingerId`を取得します。

`Update()`関数内で、

```cs
if (touch.fingerId == _fingerId)
{
     /*...*/
}
```

により、`_fingerId`と一致するidの指のみを絞り込み、`touch.Phase`で処理を分岐させて、

```cs

_rectTransform.position = touch.position;
```

により、タップしている場所にButtonを移動させています。

Buttonから指を離す時は`_fingerId`には`-1`を代入しています。

# 参考文献

- [UnityEngine.Touch - Unity スクリプトリファレンス](https://docs.unity3d.com/ja/current/ScriptReference/Touch.html)
- [how to make multi-touch - Unity Answers](https://answers.unity.com/questions/615168/how-to-make-multi-touch.html)
- [How to implement multi touch in android 2d game? - Unity Answers](https://answers.unity.com/questions/1374162/how-to-implement-multi-touch-in-android-2d-game.html)