スマホのタッチ操作

# 初めに

unityの操作でスマホのタッチ操作実装とマウスの操作を
実装する際にスマホではタッチ、pc上で確認するときにはマウスで
操作できるようにします。


## プログラム
interfaceで宣言して、初期化する際に
Application.isEditorで現在スマホ操作中なのかエディタで操作中なのかを判定しています。

```c#
using System.Linq;
using UnityEngine;

public class InputUtility
{
    public enum TouchState
    {
        Began,
        Moved,
        Stationary,
        Ended,
        None,
    }
    private static readonly TouchGetter touchGetter_;

    /// <summary>
    /// マウス操作なのかタッチ操作なのか
    /// </summary>
    static InputUtility()
    {
        touchGetter_ = Application.isEditor ? (TouchGetter)new TouchGetterUsingMouse() : (TouchGetter)new TouchGetterUsingTouch();
    }


    public static TouchState GetTouchState(int index) => touchGetter_.GetTouchState(index);

    public static Vector3 GetTouchPosition(int index) => touchGetter_.GetTouchPosition(index);

    public static int GetTouchCount() => touchGetter_.GetTouchCount();

    public static void Update() => touchGetter_.OnUpdateFrame();

    /// <summary>
    /// interface
    /// </summary>
    private interface TouchGetter
    {
        TouchState GetTouchState(int index);
        Vector3 GetTouchPosition(int index);
        int GetTouchCount();
        void OnUpdateFrame();
    }


    /// <summary>
    /// マウス用
    /// </summary>
    private class TouchGetterUsingMouse : TouchGetter
    {
        /// <summary>
        /// UnityEditor上でのInput.touchCountは常に0であるため、自前で計算し、その結果を格納する
        /// </summary>
        private int touchCount_ = 0;

        public Vector3 GetTouchPosition(int index)
        {
            var touchState = GetTouchState(index);
            if (touchState == TouchState.None)
            {
                return Vector3.zero;
            }
            return Input.mousePosition;
        }

        public TouchState GetTouchState(int index)
        {
            if (Input.GetMouseButtonDown(index)) { return TouchState.Began; }
            if (Input.GetMouseButton(index)) { return TouchState.Moved; }
            if (Input.GetMouseButtonUp(index)) { return TouchState.Ended; }
            return TouchState.None;
        }

        public int GetTouchCount()
        {
            return touchCount_;
        }

        public void OnUpdateFrame()
        {
            touchCount_ = 0;
            if (Input.GetMouseButtonDown(0) || Input.GetMouseButton(0) || Input.GetMouseButtonUp(0))
            {
                touchCount_ = 1;
            }
        }
    }

    /// <summary>
    /// タッチ用
    /// </summary>
    private class TouchGetterUsingTouch : TouchGetter
    {
        private Vector3 touchPosition_ = Vector3.zero;

        public Vector3 GetTouchPosition(int index)
        {
            if (Input.touchCount <= 0)
            {
                return Vector3.zero;
            }
            var touchs = Enumerable.Range(0, Input.touchCount).Select((i) => Input.GetTouch(i)).Where((touch) => touch.fingerId == index);
            if (!touchs.Any())
            {
                return Vector3.zero;
            }
            var touch = touchs.First();
            touchPosition_.x = touch.position.x;
            touchPosition_.y = touch.position.y;
            return touchPosition_;
        }

        public TouchState GetTouchState(int index)
        {
            if (Input.touchCount <= 0)
            {
                return TouchState.None;
            }
            var touchs = Enumerable.Range(0, Input.touchCount).Select((i) => Input.GetTouch(i)).Where((touch) => touch.fingerId == index);
            return touchs.Any() ? (TouchState)((int)touchs.First().phase) : TouchState.None; 
        }

        public int GetTouchCount()
        {
            return Input.touchCount;
        }

        public void OnUpdateFrame()
        {

        }
    }
}

```
注目点は何もあ無いemptyのオブジェクトで大丈夫です。
それぞれ、カメラをコントロールする際のオブジェクトの説明はTooltipで
書いているので問題ないと思います。

```c#
using UnityEngine;

public class CameraController : MonoBehaviour
{
    #region 変数 SerializeField

    /// 操作対象のカメラ
    [Tooltip("操作対象のカメラ")]
    [SerializeField]
	private Camera targetCamera_;

    /// 注視点のターゲットとなるもの
    [Tooltip("注視点のターゲットとなるもの")]
    [SerializeField]
	private Transform targetTransform_;

    /// 開始時のカメラとtargetTransformの距離
    [Tooltip("開始時のカメラとtargetTransformの距離")]
    [SerializeField]
	private float initialCameraDistance_;

    /// カメラからtargetTransformへの最小距離
    [Tooltip("カメラからtargetTransformへの最小距離")]
    [SerializeField]
	private float distanceMin_;

    /// カメラからtargetTransformへの最大距離
    [Tooltip("カメラからtargetTransformへの最大距離")]
    [SerializeField]
	private float distanceMax_;

    /// 回転操作時のカメラ移動速度調整用
    [Tooltip("回転操作時のカメラ移動速度調整用")]
    [SerializeField]
	private float dragSpeed_;

    /// ズーム操作時のカメラ移動速度調整用
    [Tooltip("ズーム操作時のカメラ移動速度調整用")]
    [SerializeField]
	private float zoomSpeed_;
    
	/// 平行移動操作時のカメラ移動速度調整用
    [Tooltip("平行移動操作時のカメラ移動速度調整用")]
    [SerializeField]
	private float translateSpeed_;
    
	/// ダブルタップの有効時間(秒)
    /// この時間を経過後にタップしてもダブルタップと認識しない.
    [Tooltip("ダブルタップの有効時間(秒)")]
    [SerializeField]
	private float doubleTouchValidSeconds_;

    #endregion

    #region 変数 

    /// カメラからtargetTransformへの距離
    private float distanceToTarget_;

    /// リセット用カメラ初期位置
    private Vector3 initialCameraPosition_;
    private Vector3[] previousTouchPosition_ = new Vector3[2];

    /// 回転の合計
    private Vector3 totalRotate_;
    /// 平行移動の合計
    private Vector3 totalTranslate_;

    /// ズーム時の以前のタップ2点の距離
    private float previousTouchDistanceForZoom_;
    /// baseTouchDistanceの無効判定用の定数
    private readonly float InvalidTouchDistance_ = -1.0f;

    private int touchCountForDoubleTouch_ = 0;

    private float pastSecondsFromFirstTouchForDoubleTouch_;

    /// 細かい動きの場合に判定してしまうとユーザーが意図したものと違う操作になる可能性があるため、ある程度大きな動作になった時に判定
    private readonly float MultiTouchJudgeVal_ = 5.0f;

    /// マルチタップ時の操作モード
    private enum multiTouchMode_
    {
        /// なし
        None,
        /// 平行移動モード
        Translation,
        /// ズームモード
        Zoom
    }
    private multiTouchMode_ multiTouchMode_ = multiTouchMode_.None;

	#endregion

	#region unity
	private void Reset()
	{
        initialCameraDistance_ = 8.0f;
        distanceMin_ = 2.0f;
        distanceMax_ = 20.0f;
        dragSpeed_ = 0.2f;
        zoomSpeed_ = 0.05f;
        translateSpeed_ = 0.01f;
        doubleTouchValidSeconds_ = 0.3f;
    }
    private void Start()
    {
        Setup();
    }

    private void LateUpdate()
    {
        InputUtility.Update();

        Vector3 rotateOffset = Vector3.zero;
        Vector3 translateOffset = Vector3.zero;
        float zoomDistance = 0;
        CalculateCameraAction(ref rotateOffset, ref translateOffset, ref zoomDistance);

        UpdateDoubleTouchValues();
        SetCameraPosition(rotateOffset.x, rotateOffset.y, translateOffset.x, translateOffset.y, zoomDistance);
    }

    #endregion

    private void Setup()
    {
        SetDistanceMinMax(distanceMin_, distanceMax_);
        initialCameraDistance_ = ClampDistance(initialCameraDistance_);

        SetInitialCameraPosition();
        SetCameraPosition(0, 0, 0, 0, initialCameraDistance_);
    }

    #region カメラの動きを計算する
    private void CalculateCameraAction(ref Vector3 rotate, ref Vector3 translate, ref float zoom)
    {
#if UNITY_EDITOR
        zoom = CalculateZoomByMouseWheel();
        translate = CalculateTranslateByRightButton();
#endif
        var count = InputUtility.GetTouchCount();
        if (count == 1)
        {
            CalculateMoveOnSingleTouch(ref rotate);
        }
        else if (count == 2)
        {
            CalculateMoveOnMultiTouch(ref translate, ref zoom);
        }
    }

    private void CalculateMoveOnSingleTouch(ref Vector3 rotate)
    {

        switch (InputUtility.GetTouchState(0))
        {
            case InputUtility.TouchState.Began:
                previousTouchPosition_[0] = InputUtility.GetTouchPosition(0);
                OnTouchDownForDoubleTouch();
                break;
            case InputUtility.TouchState.Stationary:
            case InputUtility.TouchState.Moved:
                var currentInputPosition = InputUtility.GetTouchPosition(0);
                rotate = CalculateRotate(currentInputPosition);
                previousTouchPosition_[0] = currentInputPosition;
                break;
        }
    }

    private void CalculateMoveOnMultiTouch(ref Vector3 translate, ref float zoom)
    {
        var firstTouchPosition = InputUtility.GetTouchPosition(0);
        var secondTouchPosition = InputUtility.GetTouchPosition(1);
        if (InputUtility.GetTouchState(1) == InputUtility.TouchState.Began)
        {
            // この時点でtouchPositionには前回のタッチ位置が入っている想定のため、初期化を行って新しい状態.
            previousTouchPosition_[0] = firstTouchPosition;
            previousTouchPosition_[1] = secondTouchPosition;
            ResetmultiTouchMode_();
        }

        switch (InputUtility.GetTouchState(0))
        {
            case InputUtility.TouchState.Began:
                OnTouchDownForDoubleTouch();
                break;
            case InputUtility.TouchState.Stationary:
            case InputUtility.TouchState.Moved:
                if (multiTouchMode_ == multiTouchMode_.None)
                {
                    multiTouchMode_ = CalculatemultiTouchMode_(firstTouchPosition, secondTouchPosition);
                }
                switch (multiTouchMode_)
                {
                    case multiTouchMode_.Translation:
                        translate = CalculateTranslate(firstTouchPosition);
                        break;
                    case multiTouchMode_.Zoom:
                        zoom = CalculateZoom(firstTouchPosition, secondTouchPosition);
                        break;
                }
                previousTouchPosition_[0] = firstTouchPosition;
                previousTouchPosition_[1] = secondTouchPosition;
                break;
            case InputUtility.TouchState.Ended:
                ResetmultiTouchMode_();
                break;
        }
        if (InputUtility.GetTouchState(1) == InputUtility.TouchState.Ended)
        {
            previousTouchPosition_[0] = InputUtility.GetTouchPosition(0);
            ResetmultiTouchMode_();
        }
    }

    private void ResetmultiTouchMode_()
    {
        multiTouchMode_ = multiTouchMode_.None;
        previousTouchDistanceForZoom_ = InvalidTouchDistance_;
    }

    /// <summary>
    /// currentPositionから回転操作時の移動量を計算する
    /// </summary>
    /// <param name="currentPosition">1つ目のタップ位置</param>
    /// <returns>回転操作時の移動量</returns>
    private Vector3 CalculateRotate(Vector3 currentPosition)
    {
        var movedPosition = currentPosition - previousTouchPosition_[0];

        return new Vector3(movedPosition.x * dragSpeed_, movedPosition.y * dragSpeed_, 0.0f);
    }

    /// <summary>
    /// firstTouchPositionとsecondTouchPositionからズーム操作時の移動量を計算する
    /// </summary>
    /// <param name="firstTouchPosition">1つ目のタップ位置</param>
    /// <param name="secondTouchPosition">2つ目のタップ位置</param>
    /// <returns>ズーム操作時の移動量</returns>
    private float CalculateZoom(Vector3 firstTouchPosition, Vector3 secondTouchPosition)
    {
        var touchDistance = Mathf.Abs((firstTouchPosition - secondTouchPosition).magnitude);
        if (previousTouchDistanceForZoom_ < 0)
        {
            previousTouchDistanceForZoom_ = touchDistance;
        }
        var touchDistanceGap = previousTouchDistanceForZoom_ - touchDistance;
        float zoomOffset = touchDistanceGap * zoomSpeed_;
        previousTouchDistanceForZoom_ = touchDistance;
        return zoomOffset;
    }

#if UNITY_EDITOR
    private float CalculateZoomByMouseWheel()
    {
        return Input.mouseScrollDelta.y;
    }

    private Vector3 CalculateTranslateByRightButton()
    {
        var rightButtonTouchPosition = InputUtility.GetTouchPosition(1);
        var state = InputUtility.GetTouchState(1);
        switch (state)
        {
            case InputUtility.TouchState.Began:
                previousTouchPosition_[1] = rightButtonTouchPosition;
                break;
            case InputUtility.TouchState.Stationary:
            case InputUtility.TouchState.Moved:
                {
                    var movedPosition = rightButtonTouchPosition - previousTouchPosition_[1];
                    previousTouchPosition_[1] = rightButtonTouchPosition;

                    return new Vector3(movedPosition.x * translateSpeed_, movedPosition.y * translateSpeed_, 0);
                }
        }
        return Vector3.zero;
    }
#endif

    /// <summary>
    /// firstTouchPositionから平行移動操作時の移動量を計算する
    /// </summary>
    /// <param name="firstTouchPosition">1つ目のタップ位置</param>
    /// <returns></returns>
    private Vector3 CalculateTranslate(Vector3 firstTouchPosition)
    {
        var movedPosition = firstTouchPosition - previousTouchPosition_[0];

        return new Vector3(movedPosition.x * translateSpeed_, movedPosition.y * translateSpeed_, 0);
    }

    /// <summary>
    /// マルチタップ時にどの操作モードに入るかを計算する
    /// </summary>
    /// <param name="firstTouchPosition">1つ目のタップ位置</param>
    /// <param name="secondTouchPosition">2つ目のタップ位置</param>
    /// <returns></returns>
    private multiTouchMode_ CalculatemultiTouchMode_(Vector3 firstTouchPosition, Vector3 secondTouchPosition)
    {
        var firstVector = firstTouchPosition - previousTouchPosition_[0];
        var secondVector = secondTouchPosition - previousTouchPosition_[1];

        if (firstVector.magnitude < MultiTouchJudgeVal_ || secondVector.magnitude < MultiTouchJudgeVal_) {
            // 細かい動きの場合に判定してしまうとユーザーが意図したものと違う操作になる可能性があるため、ある程度大きな動作になった時に判定
            return multiTouchMode_.None;
        }

        // 同じ方向に動かそうとしているか、そうでないかでモードを判定
        var dot = Vector3.Dot(firstVector.normalized, secondVector.normalized);
        return (dot > 0.5f) ? multiTouchMode_.Translation : multiTouchMode_.Zoom;
    }
    #endregion

    #region ダブルタッチ用
    private void UpdateDoubleTouchValues()
    {
        if (touchCountForDoubleTouch_ <= 0)
        {
            return;
        }
        pastSecondsFromFirstTouchForDoubleTouch_ += Time.unscaledDeltaTime;
        if (!IsValidDoubleTouch())
        {
            ResetDoubleTouchValues();
        }
    }

    private void OnTouchDownForDoubleTouch()
    {
        if (touchCountForDoubleTouch_ <= 0)
        {
            ResetDoubleTouchValues();
            touchCountForDoubleTouch_ = 1;
        }
        else
        {
            touchCountForDoubleTouch_++;
            if (IsValidDoubleTouch())
            {
                OnDoubleTouch();
            }
            ResetDoubleTouchValues();
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns>ダブルタップ有効時間内である場合true</returns>
    private bool IsValidDoubleTouch()
    {
        return doubleTouchValidSeconds_ >= pastSecondsFromFirstTouchForDoubleTouch_;
    }

    private void ResetDoubleTouchValues()
    {
        touchCountForDoubleTouch_ = 0;
        pastSecondsFromFirstTouchForDoubleTouch_ = 0;
    }

    /// <summary>
    /// ダブルタップした時用
    /// </summary>
    private void OnDoubleTouch()
    {
    }

    #endregion

    #region 距離計算

    private void SetDistanceMinMax(float min, float max)
    {
        distanceMin_ = Mathf.Min(min, max);
        distanceMax_ = Mathf.Max(min, max);
    }

    private float ClampDistance(float distance)
    {
        return Mathf.Clamp(distance, distanceMin_, distanceMax_);
    }

    private void SetDistance(float distance)
    {
        distanceToTarget_ = Mathf.Clamp(distance, distanceMin_, distanceMax_);
    }

    #endregion

    #region カメラ　関係

    private void SetInitialCameraPosition()
    {
        initialCameraPosition_ = targetTransform_.position + new Vector3(0, 0, -initialCameraDistance_);
        ResetToInitialCameraPosition();
    }

    public void ResetToInitialCameraPosition()
    {
        targetCamera_.transform.position = initialCameraPosition_;
        targetCamera_.transform.LookAt(targetTransform_);
    }

    private void SetCameraPosition(float xRotateOffset, float yRotateOffset, float xTransOffset, float yTransOffset, float distance)
    {
        // 各offset値を全体に加算する
        if (!distance.Equals(0.0f))
        {
            SetDistance(distanceToTarget_ + distance);
        }
        if (!xRotateOffset.Equals(0.0f))
        {
            totalRotate_.x = (totalRotate_.x + xRotateOffset + 360.0f) % 360.0f;
        }
        if (!yRotateOffset.Equals(0.0f))
        {
            totalRotate_.y = Mathf.Clamp(totalRotate_.y + yRotateOffset, -89.0f, 89.0f);
        }
        if (!xTransOffset.Equals(0.0f))
        {
            totalTranslate_.x -= xTransOffset;
        }
        if (!yTransOffset.Equals(0.0f))
        {
            totalTranslate_.y -= yTransOffset;
        }

        // 位置を計算して、カメラに設定する
        targetCamera_.transform.position = initialCameraPosition_;
        var xAngle = Quaternion.AngleAxis(totalRotate_.x, Vector3.up);
        var yAngle = Quaternion.AngleAxis(totalRotate_.y, targetTransform_.right * -1);
        targetCamera_.transform.position -= targetTransform_.position;
        targetCamera_.transform.position = (xAngle * yAngle) * targetCamera_.transform.position;
        targetCamera_.transform.position += targetTransform_.position;
        var directionVector = targetCamera_.transform.position - targetTransform_.position;
        targetCamera_.transform.position = targetTransform_.position + (directionVector.normalized * distanceToTarget_);
        targetCamera_.transform.LookAt(targetTransform_);
        targetCamera_.transform.Translate(totalTranslate_);
    }

    #endregion

}

```
### 最後に
実装としては、よくある操作だと思います。
自分も使うので、自分用で書きました。

何か分からない事ありましたらコメントください。