見出し画像

メニューを作る(4)・トラックパッド風GUIを作る(Unityメモ)

使い勝手はトラックパッドというよりトラックポイント

動機

「メニューを作る」シリーズの最初にも触れていますが、ゲームのGUIでスマホの横向き片手持ちでも使いやすいものを組もうとしました。
スマホの片手持ちだと親指だけでGUIを操作することになります。親指の操作だけでメニュー選択と決定が出来そうなGUIとして、トラックパッド風の入力GUIを実装してみることにしました。

制作中のゲームGUIでは、スロット(ユースケースの引数、メニュー項目)とスロットがもつ操作(「ユニット」スロットの場合、一覧表示やフィルタなど)を独立して選択します。それぞれを別の入力要素に割り当てることにしました。

現状のGUI。右端列の下側3個のボタンがスロット階層。
下端の右半分のボタン4個がスロット。画面では「ユニット」スロットを選択。
下端の左半分のボタン5個が、スロットのもつ操作。画面では「一覧」操作を選択。

「選択を切り替える(ホバー、ポイント)」という動作は親指の動きに割り当てるとして、トラックパッドの位置を選択したいスロットに割り当てると、選択したい操作を割り当てるGUI要素がありません。ここで困ったものの、トラックパッドは2次元の指定だと気づいて解決しました。そう、次元を増やせばよいです。
・トラックパッド部分:上下左右への移動を行う→スロットの選択
・「Z軸」ボタン:「前後」への移動を行う→操作の選択
前後の指定はZ軸ボタンのタップで順に切り替えることにしました。

トラックパッドの入力仕様としては、指を降ろした位置は使わず指の移動方向を検出して選択対象を切り替えるようにしました。トラックパッドは親指サイズの小ささなので、単純にトラックパッド上のポイント位置を画面の位置に対応付けても使いづらいです。そのため、トラックパッドは指の移動方向だけを検出し、移動方向に沿って選択対象を切り替える、という入力仕様にしました。

結果として、トラックパッド風GUIの操作体系は次のようになりました。
・トラックパッド部分:なでることで選択切替え。指の移動方向を検出
    上下:スロット階層を選択(ホーム→出撃→パーティなど)
    左右:同階層のスロットを選択(パーティ、任務、マップ)
・Z軸ボタン:タップで選択切替え
    前ボタン:スロットが持つ操作を選択(スロット切替、一覧、フィルタなど)
    後ろ方向:現状は未実装

トラックパッドの入力方向について、現状では斜めは使いません。現状ではスロット階層の選択とスロットの選択を同時に行えないためです。
それとZ軸ボタンの動作について、現状は一方向のリング状の切替えだけ実装しています。後ろ方向を追加すると順方向と逆方向の両方に回せるのですが、ボタンの置き場所が課題です。

実装

トラックパッド風GUIの作成は思ったよりも難しくはありませんでした。メニューGUI本体ができてから機能追加したので、必要な機能や知識は大部分そろっていました。

実装範囲の切り分け

・トラックパッドGUIは移動方向の検出と、方向に応じたイベントハンドラの実行を行う。
・トラックパッドGUIによるボタン選択の切り替えは親GUIが行う。親GUIがメニューボタンを管理しているため。
・Z軸ボタンの制御は親GUI側で行う。これも親GUIがメニューボタンを管理しているため。

親GUI側:イベントハンドラの登録

Z軸ボタンへのイベントハンドラの登録はエディタで出来ました。EventTriggerコンポーネントを使います。

「Z軸」ボタン。操作の切り替えに割り当て

親GUI側のトラックパッド関連のコードです。ボタンを選択状態にする処理のくだりは共通化できそう。

public class UIViewer : MonoBehaviour
{
    /* 前略 */
    void Start()
    {
        //トラックパッドのイベントハンドラを登録
        var handler = TrackPadCanvas.GetComponentInChildren<TrackPadHandler>();
        handler.OnMoveLeft = SelectPrevSlot;
        handler.OnMoveRight = SelectNextSlot;
        handler.OnMoveUp = SelectPrevSlotLevel;
        handler.OnMoveDown = SelectNextSlotLevel;
    }

    public void SelectPrevSlot()
    {
        //前のスロットボタンを選択
        NextSlotID = Math.Max(0, NextSlotID - 1);
        Debug.Log($"SelectPrevSlot  NextSlotID {NextSlotID}");

        //対応するボタンを選択状態にする
        //TODO: あらかじめButtonコンポーネントを取得しておきたい
        var button = SlotButtonList[NextSlotID].GetComponentInChildren<Button>();
        if (button is not null)
        {
            button.Select();
        }
    }

    /* 中略 */

    public void SelectNextOperationLoop()
    {
        //次の操作ボタンを選択
        var next = NextOpeID + 1;
        NextOpeID = (next >= CurrentOperationList.Count) ? 0 : next;

        //対応するボタンを選択状態にする
        //TODO: あらかじめButtonコンポーネントを取得しておきたい
        var button = OperationButtonList[NextOpeID].GetComponentInChildren<Button>();
        if (button is not null)
        {
            button.Selct();
        }
    }
    /* 後略 */
}


トラックパッド側:イベントハンドラの実装

トラックパッドの検出処理はドラッグイベントを取得して行います。
一般的なドラッグアンドドロップ処理ではオブジェクトを動かすため、オブジェクトが動くことでドラッグイベントが生じるものだと思っていました。
しかしオブジェクトの移動がなくても、「オブジェクト上で画面に触れたまま移動」(又はオブジェクト上でボタンを押しながらマウス移動)という動作、つまりスワイプ動作があればドラッグイベントは発生するんですね…。

トラックパッド。ドラッグイベントを用いて移動方向を検出。

トラックパッド側のコードです。スワイプの方向を決定し、方向に応じてイベントハンドラを実行します。方向は上下左右の4方向で、斜めは使いません。

public class TrackPadHandler : MonoBehaviour
{
    public UnityAction OnMoveUp;
    public UnityAction OnMoveDown;
    public UnityAction OnMoveLeft;
    public UnityAction OnMoveRight;
    //移動累積量
    private float trackDelta_x = 0.0f;
    private float trackDelta_y = 0.0f;

    [SerializeField] public float CameraPPU = 60.0f; //TODO: プロパティ化

    /*
     * スワイプ閾値[px] = ボタン寸法[px] * 係数
     *   = PPU[px/Unit] * ボタン寸法[Unit] * 係数
     */
    //使用感調整用の係数。60[fps]/6.0 = 10frameくらいのスワイプ速度を想定
    private const float SPEED_COEF = 1.0f / 6.0f;
    //スワイプ検出閾値
    private float threshold
    {
        get { return CameraPPU * UIViewer.UIButtonSize * SPEED_COEF;  }
    }

    public void OnTrackSwipeStart(BaseEventData ev)
    {
        trackDelta_x = 0.0f;
        trackDelta_y = 0.0f;
    }
    //スワイプの方向を決定。1フレームごとに判定される
    public void TrackSwipeProcess(BaseEventData ev)
    {
        //ドラッグイベントを取得
        var pointerEventData = ev as PointerEventData;

        //移動方向の判定
        // pointerEventData.delta はピクセル単位、座標系と同じく右・上方向が正
        //上下の検出を優先
        if ( Math.Abs(pointerEventData.delta.y) > 0
            && Math.Abs(pointerEventData.delta.y) > Math.Abs(pointerEventData.delta.x)
        )
        {
            //上下に動いた
            trackDelta_y += pointerEventData.delta.y;
            if (trackDelta_y >= threshold)
            {
                //移動累積量を減らす
                trackDelta_y -= threshold;
                //上に移動
                OnMoveUp.Invoke();
            }
            else if (trackDelta_y <= -threshold)
            {
                //移動累積量を減らす
                trackDelta_y += threshold;
                //下に移動
                OnMoveDown.Invoke();
            }
        }
        else if (Math.Abs(pointerEventData.delta.x) > 0)
        {
            //左右に動いた
            trackDelta_x += pointerEventData.delta.x;
            if (trackDelta_x >= threshold)
            {
                //移動累積量を減らす
                trackDelta_x -= threshold;
                //右に移動
                OnMoveRight.Invoke();
            }
            else if (trackDelta_x <= -threshold)
            {
                //移動累積量を減らす
                trackDelta_x += threshold;
                //左に移動
                OnMoveLeft.Invoke();
            }
        }
    }
}

ドラッグイベントの取得について、2点注意点があります。
・イベントハンドラに渡されるのはPointerEventDataオブジェクト
・一方、イベントハンドラの引数の型は基本型のBaseEventDataを定義する。そのためハンドラ内で型変換が必要

スワイプの判定については、現在フレームまでの移動累積量を保持しておいて、十分動いた時に「選択を移動する」と判断します。当初は移動速度に応じて選択ボタンの切り替え速度も変えようとしましたが、使いづらかったのでやめました。結果的に、ゲームコントローラーの十字キーを使うときのような実装になりました。
移動累積量とその閾値について。ドラッグイベントで得られる移動量はピクセル単位です。一方でゲーム環境のスクリーンはPCモニタやスマホなど機器ごとに解像度が異なります。そのため、解像度に依存しないボタンの設計寸法を基準として閾値を決めました。具体的には、カメラの垂直画角と解像度から求めたカメラPPU(pixels per unit)を使って、ボタンの寸法(Unity Unit)をピクセル値に換算しました。
カメラPPUはゲーム初期設定をするコンポーネントが計算していますが、解像度変更時に変化を各GUIへ反映させる必要があります。今後の課題です。

    private void SetCameraSize()
    {
        //ウィンドウ解像度を得る
        float width = (float)Screen.width;
        float height = (float)Screen.height;
        float aspect = width / height;
        // 縦倍率を設定する
        vertical_ratio = INIT_ASPECT / aspect;

        if (vertical_ratio > 1.0f)
        {
            // 全カメラのカメラsizeを設定
            m_MainCamera.orthographicSize = INIT_ORTHOGRAPHIC_SIZE * vertical_ratio;
            m_UICamera.orthographicSize = INIT_ORTHOGRAPHIC_SIZE * vertical_ratio;
        }
        //カメラ基準でのPPUを設定
        CameraPPU = height / INIT_ORTHOGRAPHIC_SIZE * vertical_ratio;
    }

使用感

スマホにバイナリを転送するのが手間だったので、スマホをサブディスプレイ化してゲーム画面を写し、使用テストをしてみました。スマホでバイナリが動くかどうか以前に、スマホを横持ちにして親指のスワイプ操作ができるかどうかを試したかったためです。

動作自体は正常でしたが、横持ちにすると親指を大きく動かせないのと、右手で操作するとGo(決定)ボタンが親指に隠されてしまい、見づらかったです。

再掲。Goボタン(>>みたいなやつ)がちょうど右の親指に隠される。親指が邪魔…

指があまり動かない、という点ではトラックパッドというよりはトラックポイント(一般名:ポインティングスティック)のような使用感でした。もっともポインティングスティックは人差し指や中指で操作しますが。個人的には少し懐かしかったです。

トラックパッド風GUI、使えなくはないのですが、もう少し改良した方が良さそうです。いっそのことポインティングスティックに寄せる?

この記事が気に入ったらサポートをしてみませんか?