Unityの操作制御周りをスマートにしてみた。

スマホでの操作周りの制御って共通化できるよね。
ただ、普通に実装したらEditor上で操作できないよね。
ってことで、操作周りの制御をスマートにしてみました。

今回の使用想定はこちら。
・スマホでのリリース向け（だけど、Editorでも確認できるように）
・操作検出とゲーム内の操作制御のロジックを完全に分けていろんなプロジェクトで使えるように

# 事前準備
操作検出はシーン毎で変わらないが、操作制御はシーン毎で変わりそう…
スマホの操作向けなので、フリック判定もあるのでコルーチンを使いたい…

ってことで、操作検出はstatic、操作制御はMonoBehaviourって感じ?
ただ、操作制御が複数あることは無さそうなので、SingletonMonoBehaviourにしておく。

今回はこちらのSingletonMonoBehaviourクラスで実装。

```C#:SingletonMonoBehaviour.cs
using UnityEngine;

namespace System.Utility
{
    public abstract class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour
    {
        private static T instance;

        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    Type t = typeof(T);

                    instance = (T)FindObjectOfType(t);
                    if (instance == null)
                    {
                        Debug.LogError(t + " をアタッチしているGameObjectはありません");
                    }
                }

                return instance;
            }
        }

        virtual protected void Awake()
        {
            // 他のGameObjectにアタッチされているか調べる.
            // アタッチされている場合は破棄する.
            if (this != Instance)
            {
                Destroy(this);
                //Destroy(this.gameObject);
                Debug.LogWarning(
                    typeof(T) +
                    " は既に他のGameObjectにアタッチされているため、コンポーネントを破棄しました." +
                    " アタッチされているGameObjectは " + Instance.gameObject.name + " です.");
                return;
            }
        }

        public static bool Validation()
        {
            Type t = typeof(T);

            instance = (T)FindObjectOfType(t);
            if (instance == null)
            {
                return false;
            }
            return true;
        }
    }
}
```

# 操作検出
今回検出したいスマホにおける操作は6つ
・タッチなし
・タッチ開始
・タッチ移動
・タッチ静止
・タッチ終了
・タッチキャンセル
マルチタッチも考慮して配列で検知情報を管理しておく

```C#:AppUtil.cs
using UnityEngine;

public static class AppUtil
{
    private static Vector3 TouchPosition = Vector3.zero;

    /// <summary>
    /// タッチ情報を取得(エディタと実機を考慮)
    /// </summary>
    /// <returns>タッチ情報。タッチされていない場合は null</returns>
    public static TouchInfo GetTouch()
    {
        if (Application.isEditor)
        {
            if (Input.GetMouseButtonDown(0))
            {
                return TouchInfo.Began;
            }
            if (Input.GetMouseButton(0))
            {
                return TouchInfo.Moved;
            }
            if (Input.GetMouseButtonUp(0))
            {
                return TouchInfo.Ended;
            }
        }
        else
        {
            if (Input.touchCount > 0)
            {
                return (TouchInfo)((int)Input.GetTouch(0).phase);
            }
        }
        return TouchInfo.None;
    }

    /// <summary>
    /// 複数のタッチ情報を取得(エディタと実機を考慮)
    /// エディタはマルチ非対応
    /// </summary>
    /// <returns>タッチ情報。タッチされていない場合は null</returns>
    public static TouchInfo[] GetTouches()
    {
        TouchInfo[] touchInfoArray = new TouchInfo[1]{TouchInfo.None};

        if (Application.isEditor)
        {
            if (Input.GetMouseButtonDown(0))
            {
                touchInfoArray[0] = TouchInfo.Began;
                    return touchInfoArray;
            }
            if (Input.GetMouseButton(0))
            {

                touchInfoArray[0] = TouchInfo.Moved;
                return touchInfoArray;
            }
            if (Input.GetMouseButtonUp(0))
            {
                touchInfoArray[0] = TouchInfo.Ended;
                return touchInfoArray;
            }
        }
        else
        {
            if (Input.touchCount > 0)
            {
                touchInfoArray = new TouchInfo[Input.touchCount];
                for (var loopValue = 0; loopValue < Input.touchCount; loopValue++)
                {
                    touchInfoArray[loopValue] = (TouchInfo)((int)Input.GetTouch(loopValue).phase);
                }
                return touchInfoArray;
            }
        }
        return touchInfoArray;
    }

    /// <summary>
    /// タッチポジションを取得(エディタと実機を考慮)
    /// </summary>
    /// <returns>タッチポジション。タッチされていない場合は (0, 0, 0)</returns>
    public static Vector3 GetTouchPosition(int id = 0)
    {
        if (Application.isEditor)
        {
            TouchInfo touch = AppUtil.GetTouch();
            if (touch != TouchInfo.None)
            {
                return Input.mousePosition;
            }
        }
        else
        {
            if (Input.touchCount > 0)
            {
                Touch touch = Input.GetTouch(id);
                TouchPosition.x = touch.position.x;
                TouchPosition.y = touch.position.y;
                return TouchPosition;
            }
        }
        return Vector3.zero;
    }

    /// <summary>
    /// タッチワールドポジションを取得(エディタと実機を考慮)
    /// </summary>
    /// <param name='camera'>カメラ</param>
    /// <returns>タッチワールドポジション。タッチされていない場合は (0, 0, 0)</returns>
    public static Vector3 GetTouchWorldPosition(Camera camera)
    {
        return camera.ScreenToWorldPoint(GetTouchPosition());
    }
}

/// <summary>
/// タッチ情報。UnityEngine.TouchPhase に None の情報を追加拡張。
/// </summary>
public enum TouchInfo
{
    /// <summary>
    /// タッチなし
    /// </summary>
    None = 99,

    // 以下は UnityEngine.TouchPhase の値に対応
    /// <summary>
    /// タッチ開始
    /// </summary>
    Began = 0,
    /// <summary>
    /// タッチ移動
    /// </summary>
    Moved = 1,
    /// <summary>
    /// タッチ静止
    /// </summary>
    Stationary = 2,
    /// <summary>
    /// タッチ終了
    /// </summary>
    Ended = 3,
    /// <summary>
    /// タッチキャンセル
    /// </summary>
    Canceled = 4,
}
```

# 操作制御
今回の目的は”操作周りをスマートに”ということなので、必要な制御を後から登録して呼び出される感じにしたい。
なので、Action使って呼び出すようにしたいと思う。

```C#:OperationSystemManager.cs
using System.Utility;
using UnityEngine;
using System.Collections.Generic;
using System.Collections;

namespace System.Operation
{
    /// <summary>
    /// 操作周りの管理クラス
    /// </summary>
    public class OperationSystemManager : SingletonMonoBehaviour<OperationSystemManager>
    {
        const float TAP_DISTANCE = 2f;
        const float FLICK_TIME = 0.15f;

        #region アクション

        public Action TapAction;
        public Action FlickAction;
        public Action<Vector3> DragAction;

        public Action CancelAction;

        #endregion

        public bool isPause;

        private List<int> touchIDList = new List<int>();
        private List<int> isFlickAdaptationIDList = new List<int>();
        private Dictionary<int,Vector3> startPosList = new Dictionary<int,Vector3>();
        private Dictionary<int,Vector3> endPosList = new Dictionary<int,Vector3>();

        void Update()
        {
            TouchInfo[] infoArray = AppUtil.GetTouches();

            if (isPause)
            {
                for (var loopValue = 0; loopValue < infoArray.Length; loopValue++)
                {
                    infoArray[loopValue] = TouchInfo.Canceled;
                }
                infoArray = new TouchInfo[]{ };
                touchIDList.Clear();
                return;
            }

            for (var loopValue = 0; loopValue < infoArray.Length; loopValue++)
            {
                switch (infoArray[loopValue])
                {
                    case TouchInfo.Began:
                        startPosList[loopValue] = AppUtil.GetTouchPosition(loopValue);
                        if (touchIDList.Contains(loopValue))
                        {
                            touchIDList.Remove(loopValue);
                        }

                        if (isFlickAdaptationIDList.Contains(loopValue))
                        {
                            isFlickAdaptationIDList.Remove(loopValue);
                        }
                        Touch(infoArray[loopValue], loopValue);
                        break;

                    case TouchInfo.Moved:
                    case TouchInfo.Stationary:
                        endPosList[loopValue] = AppUtil.GetTouchPosition(loopValue);
                        Touch(infoArray[loopValue], loopValue);
                        break;

                    case TouchInfo.Ended:
                    case TouchInfo.Canceled:
                        endPosList[loopValue] = AppUtil.GetTouchPosition(loopValue);
                        Touch(infoArray[loopValue], loopValue);
                        if (touchIDList.Contains(loopValue))
                        {
                            touchIDList.Remove(loopValue);
                        }

                        if (isFlickAdaptationIDList.Contains(loopValue))
                        {
                            isFlickAdaptationIDList.Remove(loopValue);
                        }
                        break;
                }
            }
        }

        void Touch(TouchInfo info, int id)
        {
            if (info == TouchInfo.Began)
            {
                StartCoroutine(FlickTimer(id, isFlickAdaptationIDList, FLICK_TIME));
            }
            else if (info == TouchInfo.Moved || info == TouchInfo.Stationary)
            {
                if (touchIDList.Contains(id))
                {
                    info = TouchInfo.Canceled;

                    var isRemove = false;
                    if (startPosList.ContainsKey(id))
                    {
                        startPosList.Remove(id);
                        isRemove = true;
                    }
                    if (endPosList.ContainsKey(id)){
                        endPosList.Remove(id);
                        isRemove = true;
                    }
                    if (isRemove && CancelAction != null)
                    {
                        CancelAction();
                    }
                    return;
                }

                if (!isTap(id))
                {
                    if (!isFlickAdaptationIDList.Contains(id))
                    {
                        // ドラッグ処理
                        if (DragAction != null)
                        {
                            if (startPosList.ContainsKey(id) && endPosList.ContainsKey(id))
                            {
                                DragAction((endPosList[id]-startPosList[id]).normalized);
                            }
                        }
                    }
                }
            }
            else if (info == TouchInfo.Ended || info == TouchInfo.Canceled)
            {
                if (touchIDList.Contains(id))
                    return;
                
                if (isTap(id))
                {
                    // タップ処理
                    if (TapAction != null)
                    {
                        TapAction();
                    }
                }
                else
                {
                    if (isFlickAdaptationIDList.Contains(id))
                    {
                        // フリック処理
                        if (startPosList[id].y - endPosList[id].y < 0)
                        {
                            if (FlickAction != null)
                            {
                                FlickAction();
                            }
                        }
                    }
                    else
                    {
                        // ドラッグ処理
                        if (DragAction != null)
                        {
                            DragAction(Vector3.zero);
                        }
                    }
                }
            }
        }

        bool isTap(int id)
        {
            if (!startPosList.ContainsKey(id) || !endPosList.ContainsKey(id))
                return false;
            
            if (Vector3.Distance(startPosList[id], endPosList[id]) < TAP_DISTANCE)
                return true;

            return false;
        }

        IEnumerator FlickTimer(int id,List<int> isFlickAdaptationIDList,float time)
        {
            if(!isFlickAdaptationIDList.Contains(id))
            {
                isFlickAdaptationIDList.Add(id);
            }
            yield return new WaitForSeconds(time);
            if(isFlickAdaptationIDList.Contains(id))
            {
                isFlickAdaptationIDList.Remove(id);
            }
        }

    }
}
```

下準備は完了したので、どう使用できるか確認してみる。
# 使用例
使用例はこんな感じ！

```C#:Test.cs一部抜粋
if (!OperationSystemManager.Validation())
    return;
OperationSystemManager.Instance.TapAction += TapAction;
```

OperationSystemManagerのActionにゲーム内の制御を登録しておくだけ！
めちゃくちゃスマートになったんじゃない？

今回用意したActionはコレ。
TapAction：タップした時に呼ばれる
FlickAction：フリックした時に呼ばれる
DragAction：ドラッグした時に呼ばれる（引数にドラッグ方向がVector3で貰えます）
CancelAction：キャンセルした時に呼ばれる

# まとめ
今回の実装で操作周りがスマートになったので、ハッカソンでも大活躍しそうな予感！
DragActionを拡張してドラッグした距離を取得できたら良かったかも？
引っ張りアクション系を想定するとまだまだ使いにくい部分がありそうなので、DragStartAction,DragEndActionとか用意してより使いやすいように改良しよう…