【Unity】ゆるモン作成日記③ 〜ジョイスティックとカメラ移動の実装〜

スポンサーリンク
PC創作
マイケル
マイケル
どうもみなさんこんにちは!
マイケルです!
エレキベア
エレキベア
クマ〜〜〜〜〜
マイケル
マイケル
とりあえずゴーゴーゴロヤンの開発が終わったので、
久しぶりにゆるモンの作成を進めてみようと思います!

↓前回の作成日記はこちら

【Unity】ゆるモン作成日記② 〜追加キャラ・エフェクト・効果音作成まで〜

エレキベア
エレキベア
ほんと久しぶりクマね〜〜〜
もう作るのやめてしまったのかと思ったクマ
マイケル
マイケル
時間がかかりそうだし、仕様もきっちりと決まってないから
他の作りながら少しずつ進めていこうと思ってるよ!
マイケル
マイケル
今回はこんな感じで修正してみました!
RPReplay Final1595495769
エレキベア
エレキベア
お、スティックが増えてるクマ
マイケル
マイケル
よく気づいたね!
これまでキー操作しかできなかったけど、
スマホで遊ぶのを想定してジョイスティックを追加で実装したんだ!
マイケル
マイケル
それと合わせて画面ドラッグでカメラ移動する処理も追加したよ
エレキベア
エレキベア
今回はこのスティックの実装方法について話すクマね
マイケル
マイケル
そういうことだ!
早速進めていこう!
スポンサーリンク

参考にした書籍について

マイケル
マイケル
まず実装にあたって参考にした参考書は以下になります!
Screenshot 2020 07 23 18 44 34


Unityゲーム プログラミング・バイブル

マイケル
マイケル
こちらの「3Dアクション」の章を参考にさせてもらいました!
書籍ではタップした箇所にスティックが現れるようにしていますが、
今回はスティック位置は固定で移動するようカスタマイズして組み込んだよ!
マイケル
マイケル
実践的なサンプルがたくさん載っているので、
初心者向けの参考書を読破した次のステップとしておすすめの参考書です!
エレキベア
エレキベア
この本はクマも持ってるクマ〜〜〜〜
スポンサーリンク

ジョイスティックの実装

マイケル
マイケル
それでは早速実装していこう!

スティックオブジェクトの作成

マイケル
マイケル
まずは、スティックの固定部分と動かす部分の画像を用意します!
Stick out
Stick in
マイケル
マイケル
画像ができたら、UnityでCanvasに追加しましょう!
こちらのオブジェクトを使用して移動処理を実装していきます!
Screenshot 2020 07 23 18 55 36
エレキベア
エレキベア
円かいてポイクマね

移動処理の実装

マイケル
マイケル
それでは移動処理をスクリプトで実装します!
実装したソースは以下になります!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class testStickMove : MonoBehaviour
{
    // ********************
    // * 変数定義          *
    // ********************

    /** コンポーネント */
    PlayerCtrl playerCtrl;

    /** 定数 */
    private const float STICK_RADIUS_MAX = 65.0f; // スティック最大半径
    private const float STICK_RADIUS_MIN = 20.0f; // スティック最小半径

    /** UIオブジェクト */
    GameObject UICanvas;
    GameObject inStickButton;
    Vector3 inStickButtonInitPosition;

    /** 変数 */
    bool moveStick = false;    // スティック移動フラグ
    private Touch stickTouch;  // スティックタッチ情報


    // ********************
    // * 処理実行          *
    // ********************

・・・略・・・

    void Update()
    {
        // スティック移動更新処理
        UpdateStickMove();
    }

    // スティック移動更新処理
    private void UpdateStickMove()
    {
        // スティックボタン移動中の場合
        if (moveStick)
        {
            // スクリーン座標で計算する場合、様々な解像度を考慮する必要があるので
            // キャンバス座標で計算を行う
            var screenDragPosition = GetCurrentTouchPosition(stickTouch);
            var screenTapPosition = inStickButtonInitPosition;
            Vector2 canvasDragPosition = ScreenToCanvas(screenDragPosition);
            Vector2 canvasTapPosition = ScreenToCanvas(screenTapPosition);

            // スワイプした差分を算出
            Vector2 diffPosition = canvasDragPosition - canvasTapPosition;
            var diffSqrMagnitude = diffPosition.sqrMagnitude;

            // 最大半径より大きいなら丸める
            var stickSqrRadiusMax = STICK_RADIUS_MAX * STICK_RADIUS_MAX;
            if (stickSqrRadiusMax < diffSqrMagnitude)
            {
                diffPosition.Normalize();
                diffPosition *= STICK_RADIUS_MAX;
                diffSqrMagnitude = stickSqrRadiusMax;
            }

            // スティックボタンを移動
            inStickButton.GetComponent<Image>().rectTransform.localPosition =
                canvasTapPosition + diffPosition;

            // 最小半径より大きいなら移動開始
            var stickSqrRadiusMin = STICK_RADIUS_MIN * STICK_RADIUS_MIN;
            if (stickSqrRadiusMin < diffSqrMagnitude)
            {
                // 移動量を渡す
                playerCtrl.SendMessage("OnStickMove", diffPosition);
            }
            else
            {
                // 移動停止
                playerCtrl.SendMessage("OnStickMoveStop");
            }
        }
    }

    // --------------- ボタン入力操作 ---------------

    // 移動スティックを押している間
    public void PushDownStick()
    {
        moveStick = true;
        stickTouch = GetTouch();
    }

	// 移動スティックを離した時
	public void PushUpStick()
    {
        moveStick = false;
        stickTouch = NotTouch();
        // 初期位置に戻す
        inStickButton.transform.position = inStickButtonInitPosition;
        // 移動停止
        playerCtrl.SendMessage("OnStickMoveStop");
    }

	// --------------- タップ操作共通 ---------------

    // 開始されたタッチオブジェクトを返却する
    private Touch GetTouch()
	{
        if (Application.isEditor)
        {
            // *** Unityエディター ***
            // クリックされている場合(ID:0固定)
            if (Input.GetButtonDown(MOUSE_LEFT_CLICK) || Input.GetButton(MOUSE_LEFT_CLICK))
            {
                return new Touch()
                {
                    fingerId = 0,
                    position = Input.mousePosition,
                };
            }
            // クリックされていない場合
            return NotTouch();
        }
        else
        {
            // *** iOS・Android ***
            // 開始されたタッチオブジェクトを返す
            foreach (Touch t in Input.touches)
            {
                if (t.phase == TouchPhase.Began)
                    return t;
            }
            // タッチされていない場合
            return NotTouch();
        }
    }

    // 初期化用のタッチオブジェクトを返却する
    private Touch NotTouch()
    {
        // タッチされていない場合(ID: -1固定)
        return new Touch()
        {
            fingerId = -1,
        };
    }

    // タッチIDに対する現在位置を取得
    private Vector3 GetCurrentTouchPosition(Touch touch)
    {
        if (Application.isEditor)
        {
            // *** Unityエディター ***
            // マウスの位置を返却
            return Input.mousePosition;
        }
        else
        {
            // *** iOS・Android ***
            // タッチIDの位置を返却
            Vector3 currentPosition = new Vector3(0, 0, 0);
            foreach (Touch t in Input.touches)
            {
                if (t.fingerId == touch.fingerId)
                    currentPosition = t.position;
            }
            return currentPosition;
        }
    }

    // スクリーン座標からキャンバス座標に変換
    private Vector3 ScreenToCanvas(Vector3 screenPos)
    {
        // スクリーン座標を割合に変換
        float ratioX = screenPos.x / Screen.width;
        float ratioY = screenPos.y / Screen.height;

        // 割合をキャンバス座標に当てはめる.
        var canvasRect = UICanvas.GetComponent<RectTransform>().rect;
        float canvasX = ratioX * canvasRect.width;
        float canvasY = ratioY * canvasRect.height;

        // キャンバス座標は中心が0なので合わせる.
        canvasX -= canvasRect.width * 0.5f;
        canvasY -= canvasRect.height * 0.5f;

        return new Vector3()
        {
            x = canvasX,
            y = canvasY,
            z = 0.0f,
        };
    }
}
public class PlayerCtrl : MonoBehaviour
{

・・・略・・・

    // スティック移動処理
    public void OnStickMove(Vector2 direction)
    {
        // 歩行ステータスの場合のみ移動
        if (!(state == State.Running))
            return;

        // カメラの回転分、向きも回転
        var cameraForward = followCamera.transform.forward;
        cameraForward.y = 0.0f;

        var cameraRotation = Quaternion.LookRotation(cameraForward);
        var directionXZ = new Vector3(direction.x, 0.0f, direction.y);
        var directionFromCamera = cameraRotation * directionXZ;
        directionFromCamera.Normalize();

        // 移動先を渡す
        SendMessage("SetDestination", directionFromCamera * 70.0f * Time.deltaTime + transform.localPosition);
    }

    // スティック移動停止処理
    public void OnStickMoveStop()
    {
        // 移動を停止させる
        SendMessage("StopMove");
    }
}
マイケル
マイケル
Unity.Inputクラスの「UnityEngine.Touch」を使用して
タップを検出するようにしています!
Screenshot 2020 07 23 19 27 40
マイケル
マイケル
「UnityEngine.Touch」では、タップされるごとに「fingerID」が一意に割り当てられます。
割り当てられたIDを使用してタップが継続しているかどうかを判定するようにしています!
エレキベア
エレキベア
スマホ用に複数タップに対応したクマね

・・・略・・・

    // --------------- ボタン入力操作 ---------------

    // 移動スティックを押している間
    public void PushDownStick()
    {
        moveStick = true;
        stickTouch = GetTouch();
    }

	// 移動スティックを離した時
	public void PushUpStick()
    {
        moveStick = false;
        stickTouch = NotTouch();
        // 初期位置に戻す
        inStickButton.transform.position = inStickButtonInitPosition;
        // 移動停止
        playerCtrl.SendMessage("OnStickMoveStop");
    }

	// --------------- タップ操作共通 ---------------

    // 開始されたタッチオブジェクトを返却する
    private Touch GetTouch()
	{
        if (Application.isEditor)
        {
            // *** Unityエディター ***
            // クリックされている場合(ID:0固定)
            if (Input.GetButtonDown(MOUSE_LEFT_CLICK) || Input.GetButton(MOUSE_LEFT_CLICK))
            {
                return new Touch()
                {
                    fingerId = 0,
                    position = Input.mousePosition,
                };
            }
            // クリックされていない場合
            return NotTouch();
        }
        else
        {
            // *** iOS・Android ***
            // 開始されたタッチオブジェクトを返す
            foreach (Touch t in Input.touches)
            {
                if (t.phase == TouchPhase.Began)
                    return t;
            }
            // タッチされていない場合
            return NotTouch();
        }
    }

    // 初期化用のタッチオブジェクトを返却する
    private Touch NotTouch()
    {
        // タッチされていない場合(ID: -1固定)
        return new Touch()
        {
            fingerId = -1,
        };
    }

・・・略・・・

}
マイケル
マイケル
まず上記「PushDownStick()」メソッドでスティックのタップを検知し、
「GetTouch()」メソッドでタップされたTouch情報を取得しています!
マイケル
マイケル
「GetTouch()」メソッドでは、「TouchPhase.Began」で開始された情報を返却しており、Touch情報がない場合にはfingerIDを「-1」で指定しています。
一方Unityエディターではマウスクリックのみなので、fingerIDは「0」で固定しています。

・・・略・・・

    void Update()
    {
        // スティック移動更新処理
        UpdateStickMove();
    }

    // スティック移動更新処理
    private void UpdateStickMove()
    {
        // スティックボタン移動中の場合
        if (moveStick)
        {
            // スクリーン座標で計算する場合、様々な解像度を考慮する必要があるので
            // キャンバス座標で計算を行う
            var screenDragPosition = GetCurrentTouchPosition(stickTouch);
            var screenTapPosition = inStickButtonInitPosition;
            Vector2 canvasDragPosition = ScreenToCanvas(screenDragPosition);
            Vector2 canvasTapPosition = ScreenToCanvas(screenTapPosition);

            // スワイプした差分を算出
            Vector2 diffPosition = canvasDragPosition - canvasTapPosition;
            var diffSqrMagnitude = diffPosition.sqrMagnitude;

            // 最大半径より大きいなら丸める
            var stickSqrRadiusMax = STICK_RADIUS_MAX * STICK_RADIUS_MAX;
            if (stickSqrRadiusMax < diffSqrMagnitude)
            {
                diffPosition.Normalize();
                diffPosition *= STICK_RADIUS_MAX;
                diffSqrMagnitude = stickSqrRadiusMax;
            }

            // スティックボタンを移動
            inStickButton.GetComponent<Image>().rectTransform.localPosition =
                canvasTapPosition + diffPosition;

            // 最小半径より大きいなら移動開始
            var stickSqrRadiusMin = STICK_RADIUS_MIN * STICK_RADIUS_MIN;
            if (stickSqrRadiusMin < diffSqrMagnitude)
            {
                // 移動量を渡す
                playerCtrl.SendMessage("OnStickMove", diffPosition);
            }
            else
            {
                // 移動停止
                playerCtrl.SendMessage("OnStickMoveStop");
            }
        }
    }

・・・略・・・

    // タッチIDに対する現在位置を取得
    private Vector3 GetCurrentTouchPosition(Touch touch)
    {
        if (Application.isEditor)
        {
            // *** Unityエディター ***
            // マウスの位置を返却
            return Input.mousePosition;
        }
        else
        {
            // *** iOS・Android ***
            // タッチIDの位置を返却
            Vector3 currentPosition = new Vector3(0, 0, 0);
            foreach (Touch t in Input.touches)
            {
                if (t.fingerId == touch.fingerId)
                    currentPosition = t.position;
            }
            return currentPosition;
        }
    }

    // スクリーン座標からキャンバス座標に変換
    private Vector3 ScreenToCanvas(Vector3 screenPos)
    {
        // スクリーン座標を割合に変換
        float ratioX = screenPos.x / Screen.width;
        float ratioY = screenPos.y / Screen.height;

        // 割合をキャンバス座標に当てはめる.
        var canvasRect = UICanvas.GetComponent<RectTransform>().rect;
        float canvasX = ratioX * canvasRect.width;
        float canvasY = ratioY * canvasRect.height;

        // キャンバス座標は中心が0なので合わせる.
        canvasX -= canvasRect.width * 0.5f;
        canvasY -= canvasRect.height * 0.5f;

        return new Vector3()
        {
            x = canvasX,
            y = canvasY,
            z = 0.0f,
        };
    }
}
マイケル
マイケル
あとは取得したTouch情報を使用して、移動された量を計算したり、
最大半径を超えたら丸めたりなど、煮るなり焼くなりしましょう!
public class PlayerCtrl : MonoBehaviour
{

・・・略・・・

    // スティック移動処理
    public void OnStickMove(Vector2 direction)
    {
        // 歩行ステータスの場合のみ移動
        if (!(state == State.Running))
            return;

        // カメラの回転分、向きも回転
        var cameraForward = followCamera.transform.forward;
        cameraForward.y = 0.0f;

        var cameraRotation = Quaternion.LookRotation(cameraForward);
        var directionXZ = new Vector3(direction.x, 0.0f, direction.y);
        var directionFromCamera = cameraRotation * directionXZ;
        directionFromCamera.Normalize();

        // 移動先を渡す
        SendMessage("SetDestination", directionFromCamera * 70.0f * Time.deltaTime + transform.localPosition);
    }

    // スティック移動停止処理
    public void OnStickMoveStop()
    {
        // 移動を停止させる
        SendMessage("StopMove");
    }
}
マイケル
マイケル
移動量を受け取ったら、カメラの向きを考慮して移動先に変換してあげます!
(渡された移動先に移動する処理についてはここでは省略します)
エレキベア
エレキベア
これでスティックで動くようになったクマね〜〜〜
スポンサーリンク

カメラ移動処理の実装

マイケル
マイケル
Touchオブジェクトを勉強したついでに、
カメラの移動処理も追加しました!
マイケル
マイケル
ボタン以外の箇所をドラッグした時に、カメラも移動するようにしています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class testCameraMove : MonoBehaviour
{

    /** 定数 */
    const string MOUSE_LEFT_CLICK = "Fire1"; // キーマップ: 左クリック
    private const float STICK_RADIUS_MAX = 65.0f; // スティック最大半径
    private const float STICK_RADIUS_MIN = 20.0f; // スティック最小半径

    /** 変数 */
    Vector2 delta;              // スライド移動量
    Vector2 prevPosition;       // 前フレームのカーソル位置
    bool isSlide = false;       // スライドフラグ

    bool moveCamera = false;   // カメラ移動フラグ
    private Touch cameraTouch; // カメラタッチ情報


    // ********************
    // * 処理実行          *
    // ********************

    private void Start()
    {
        // タッチオブジェクト初期化
        stickTouch = NotTouch();
        cameraTouch = NotTouch();
    }

    void Update()
    {
        // スティック移動更新処理
        UpdateStickMove();

        // カメラ移動更新処理
        UpdateCameraMove();
    }

・・・略・・・

    // カメラ移動更新処理
    private void UpdateCameraMove()
    {
        if (!moveCamera)
        {
            // カメラ移動でない場合
            Touch beginTouch = GetTouch();
            // 新規タップがスティックボタンでないかつuGUIと重なっていない場合
            if (beginTouch.fingerId != -1 &&
                stickTouch.fingerId != beginTouch.fingerId &&
                !IsPointerOver(beginTouch))
            {
                moveCamera = true;
                cameraTouch = beginTouch;
                prevPosition = cameraTouch.position;
            }
        }
        else
        {
            // カメラ移動中の場合
            // スライド中かどうか
            isSlide = IsSlide(cameraTouch);
            // カメラ移動処理
            if (isSlide)
            {
                // スライド中の場合、移動量を求める
                Vector2 currentPosition = GetCurrentTouchPosition(cameraTouch);
                delta = currentPosition - prevPosition;
                prevPosition = currentPosition;
            }
            else
            {
                // スライド終了した場合、初期化
                delta = Vector2.zero;
                prevPosition = Vector2.zero;
                moveCamera = false;
                cameraTouch = NotTouch();

            }
        }
    }

	// --------------- タップ操作共通 ---------------

・・・略・・・

    // タップがuGUIと重なっているか
    private bool IsPointerOver(Touch touch)
    {
        if (Application.isEditor)
        {
            // *** Unityエディター ***
            return EventSystem.current &&
                EventSystem.current.IsPointerOverGameObject();
        }
        else
        {
            // *** iOS・Android ***
            return EventSystem.current &&
                EventSystem.current.IsPointerOverGameObject(touch.fingerId);
        }
    }

    // タッチがスライド中かどうか
    private bool IsSlide(Touch touch)
    {
        bool isSlide = false;
        if (Application.isEditor)
        {
            // *** Unityエディター ***
            // クリックされているかどうか
            isSlide = Input.GetButton(MOUSE_LEFT_CLICK);
        }
        else
        {
            // *** iOS・Android ***
            // タッチIDが残っているか
            foreach (Touch t in Input.touches)
            {
                if (t.fingerId == touch.fingerId)
                    isSlide = true;
            }
        }
        return isSlide;
    }

・・・略・・・

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FollowCamera : MonoBehaviour
{
    // ********************
    // * 変数定義          *
    // ********************

    /** コンポーネント */
    InputManager inputManager;

    /** 設定値 */
    public Transform player;                         // プレイヤー
    public float horizontalAngle = 360.0f;           // カメラ角度(横方向)
    public float verticalAngle = -15.0f;             // カメラ角度(縦方向)
    public Vector3 offset = new Vector3(0, 1.5f, 0); // カメラ位置のオフセット量
    public float distance = -5.0f;                   // 追跡対称とのオフセット量
    public float rotAngle = 180.0f;                  // 画面の横幅分カーソル移動時の回転量
    public float rotSpeed = 4.5f;                    // 横移動時のカメラ移動速度
    public float cameraMoveSpeed = 10.0f;            // カメラ移動モード時の移動速度

    /** 変数 */
    Transform lookTarget; // 追跡対象

    // ********************
    // * 処理実行          *
    // ********************

    void Start()
    {
        inputManager = FindObjectOfType<InputManager>();
    }

    void Update()
    {
        // ドラッグ入力でカメラアングルを更新する
        if (inputManager.Slided())
        {
            float anglePerPixel = rotAngle / (float)Screen.width;
            Vector2 delta = inputManager.GetDeltaPosition();
            horizontalAngle += delta.x * anglePerPixel;
            horizontalAngle = Mathf.Repeat(horizontalAngle, 360.0f);
            verticalAngle += delta.y * anglePerPixel;
            verticalAngle = Mathf.Clamp(verticalAngle, -60.0f, 60.0f);
        }

・・・略・・・
        // カメラの位置と回転を更新する
        // 注視対象の位置 + オフセット
        Vector3 lookPosition = player.position + offset;
        // 注視対象からの相対距離を求める
        Vector3 relativePos;
        relativePos = Quaternion.Euler(verticalAngle, horizontalAngle, 0) * new Vector3(0, 0, -distance);
        transform.position = lookPosition + relativePos;
・・・略・・・

    }

・・・略・・・

}
マイケル
マイケル
スティック移動と同様に、touch情報を使用してドラッグ判定をしています。
「EventSystem.current.IsPointerOverGameObject」を使用すればuGUIの重なりを判定できるので、ボタンの上のタッチは検出しないようにできます!
スポンサーリンク

おわりに

マイケル
マイケル
というわけで、Touchオブジェクトを使用した
ジョイスティックとカメラ移動の実装でした!
エレキベア
エレキベア
Touchオブジェクトはなかなか汎用性が高そうクマね〜〜〜
マイケル
マイケル
タップ情報をコントロールできるから、
いろんなゲームに使えると思うよ!
マイケル
マイケル
今後も少しずつ、ゆるモン進めていこうと思うので
気長に見守っていてください・・・。
エレキベア
エレキベア
完成はいつになるやらクマ・・・・。
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!
エレキベア
エレキベア
クマ〜〜〜〜〜〜〜〜

【Unity】ゆるモン作成日記③ 〜ジョイスティックとカメラ移動の実装〜 〜完〜

コメント

  1. ザード より:

    投稿ありがとうございます。
    スティックの操作知りたかったのでとても助かりました!
    ほんとに神っす……!
    色んなシリーズ見させて頂いてるのですが、とても面白いのでYouTubeとかして欲しいです!!!

    • マイケル マイケル より:

      コメントありがとうございます。
      そう言っていただけてすごくうれしいです!
      YOUTUBE・・・!!前向きに検討してみます!( ̄^ ̄)ゞ
      これからもお互いがんばりましょう!