メタバースにおける操作方法を考える 〜バーチャルPayPayドームの操作の実装〜

# はじめに

こんにちは！現在watnowという学生IT団体の運営メンバーをしています、山口結己と申します。

タイトルにもあります通り、この記事では、メタバースにおける操作方法について書いています。現在、C#を使ったメタバース開発を行なっているのですが、メタバース空間内では、常に高い操作性を目指すことを意識して開発しています。

今回は、[バーチャルPayPayドーム](https://virtualdome.mb.softbank.jp/service/?utm_source=softbank_owned&utm_medium=hp&utm_campaign=campaign_top)で実装された操作を自分で再現してみました。操作性としてはどの年齢層にも対応できるようなもので、割と直感的に操作できます。ソースコードも載せてありますので、ぜひ採用してみてください！

## 用件定義
### 1. WASD, 矢印キーでの移動
メタバース空間を移動するために必要な機能です。矢印キーでも移動可能にしています。

### 2. 視点操作は画面左半分をドラッグ
ドラッグ時は、どこにカーソルがあるかがわかるように、カーソルは消えないようにしています。

### 3. 画面右半分にバーチャルパッドを設置
バーチャルパッドは、JoyStick Packという無料で使用できるアセットがあったので、こちらを使わせていただきました。


https://assetstore.unity.com/packages/tools/input-management/joystick-pack-107631



# 実装手順
## 1. UIオブジェクトによる画面の分割
Canvas内に、左画面に新規Imageオブジェクト(青色)、JoyStick Pack内のPrefabsの中のFloating Joystickを右側（白色）に配置。

![スクリーンショット 2022-12-02 20.29.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609178/1738ed24-0a6b-68db-0d65-2730914450b9.png)



## 2. ドラッグ操作を可能にするための``DragHandler``クラスを作成
```DragHandler.cs
using System;
using UnityEngine;
using UnityEngine.EventSystems;

public class DragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public Action<PointerEventData> OnBeginDragEvent;

    public Action<PointerEventData> OnDragEvent;

    public Action<PointerEventData> OnEndDragEvent;

    public void OnBeginDrag(PointerEventData eventData)
    {
        OnBeginDragEvent?.Invoke(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        OnDragEvent?.Invoke(eventData);
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        OnEndDragEvent?.Invoke(eventData);
    }
}
```

**スクリプトを保存し、先ほど作成した２つのUIオブジェクトアタッチする。**　

## 3. ドラッグ量に応じて、プレイヤー移動のためのコードを記述
```PlayerMovement.cs
using UnityEngine;
using UnityEngine.EventSystems;

public class PlayerMovement : MonoBehaviour
{
    /// <summary> 移動操作を受け付けるタッチエリア </summary>
    [SerializeField]
    private DragHandler _moveController;

    /// <summary> 移動速度（m/秒） </summary>
    [SerializeField]
    private float _movePerSecond = 7f;

    /// <summary> 移動操作としてタッチ開始したスクリーン座標 </summary>
    private Vector2 _movePointerPosBegin;

    private Vector3 _moveVector;

    /// <summary> 起動時 </summary>
    private void Awake()
    {
        _moveController.OnBeginDragEvent += OnBeginDragMove;
        _moveController.OnDragEvent += OnDragMove;
        _moveController.OnEndDragEvent += OnEndDragMove;
    }

    /// <summary> 更新処理 </summary>
    private void Update()
    {
        UpdateMove(_moveVector);

        float horizotalValue = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        transform.position += this.transform.forward * vertical * 0.1f
                                 + this.transform.right * horizotalValue * 0.1f;
    }

    ////////////////////////////////////////////////////////////
    /// 移動操作
    ////////////////////////////////////////////////////////////
    #region Move

    /// <summary> ドラッグ操作開始（移動用） </summary>
    private void OnBeginDragMove(PointerEventData eventData)
    {
        // タッチ開始位置を保持
        _movePointerPosBegin = eventData.position;
    }

    /// <summary> ドラッグ操作中（移動用） </summary>
    private void OnDragMove(PointerEventData eventData)
    {
        // タッチ開始位置からのスワイプ量を移動ベクトルにする
        var vector = eventData.position - _movePointerPosBegin;
        _moveVector = new Vector3(vector.x, 0f, vector.y);
    }

    private void UpdateMove(Vector3 vector)
    {
        // 現在向きを基準に、入力されたベクトルに向かって移動
        transform.position += transform.rotation * vector.normalized * _movePerSecond * Time.deltaTime;
    }

    /// <summary> ドラッグ操作終了（移動用） </summary>
    private void OnEndDragMove(PointerEventData eventData)
    {
        // 移動ベクトルを解消
        _moveVector = Vector3.zero;
    }
    #endregion
}
```

## 4. ドラッグ量に応じて、カメラの向きを調節するためのコードを記述
### 4-1. `DragHandler.cs`にキャンバス上の座標取得する処理を追加

カメラ回転はスワイプ量によって回転角度が変わる処理を扱います。
**dpiによって操作感が変わることを避ける**ため、タッチしたスクリーン座標をキャンバス上でのタッチ座標（ひいては操作量）に変換する処理を追加します。

### メンバ追加
```DragHandler.cs
/// <summary> 自身が所属してるキャンバス </summary>
private Canvas _belongedCanvas;
```

### メソッド追加
```DragHandler.cs
/// <summary>
/// スクリーン座標を自身が所属してるキャンバス上の座標に変換
/// </summary>
/// <param name="pointerPos">クリーン座標</param>
/// <returns>自身が所属してるキャンバス上の座標</returns>
public Vector2 GetPositionOnCanvas(Vector2 pointerPos)
{
    if(_belongedCanvas == null)
    {
        _belongedCanvas = GetBelongedCanvas(transform);
    }
    RectTransformUtility.ScreenPointToLocalPointInRectangle(_belongedCanvas.transform as RectTransform,
        pointerPos, _belongedCanvas.worldCamera, out Vector2 localPointerPos);
    return localPointerPos;
}

/// <summary> 所属するCanvasを取得 </summary>
private Canvas GetBelongedCanvas(Transform t)
{
    if (t == null)
    {
        return null;
    }

    var canvas = t.GetComponent<Canvas>();
    if (canvas != null)
    {
        return canvas;
    }

    return GetBelongedCanvas(t.parent);
}
```

### 4-2. `PlayerMovement.cs`にカメラ操作に関する処理を追加
### メンバ追加
```PlayerMovement.cs
[SerializeField]
private Camera _camera;

/// <summary> カメラ操作を受け付けるタッチエリア </summary>
[SerializeField]
private DragHandler _lookController;

/// <summary> カメラ速度（°/px） </summary>
[SerializeField]
private float _angularPerPixel = 1f;

/// <summary> カメラ操作として前フレームにタッチしたキャンバス上の座標 </summary>
private Vector2 _lookPointerPosPre;
```

### Awake処理追加
```PlayerMovement.cs
private void Awake()
{
    _moveController.OnBeginDragEvent += OnBeginDragMove;
    _moveController.OnDragEvent += OnDragMove;
    _moveController.OnEndDragEvent += OnEndDragMove;

    // ここからが追加分
    _lookController.OnBeginDragEvent += OnBeginDragLook;
    _lookController.OnDragEvent += OnDragLook;
}
```

### メソッド追加
```PlayerMovement.cs
////////////////////////////////////////////////////////////
/// カメラ操作
////////////////////////////////////////////////////////////
#region Look
/// <summary> ドラッグ操作開始（カメラ用） </summary>
private void OnBeginDragLook(PointerEventData eventData)
{
    _lookPointerPosPre = _lookController.GetPositionOnCanvas(eventData.position);
}

/// <summary> ドラッグ操作中（カメラ用） </summary>
private void OnDragLook(PointerEventData eventData)
{
    var pointerPosOnCanvas = _lookController.GetPositionOnCanvas(eventData.position);
    // キャンバス上で前フレームから何px操作したかを計算
    var vector = pointerPosOnCanvas - _lookPointerPosPre;
    // 操作量に応じてカメラを回転
    LookRotate(new Vector2(-vector.y, vector.x));
    _lookPointerPosPre = pointerPosOnCanvas;
}

private void LookRotate(Vector2 angles)
{
    Vector2 deltaAngles = angles * _angularPerPixel;
    transform.eulerAngles += new Vector3(0f, deltaAngles.y);
    _camera.transform.localEulerAngles += new Vector3(deltaAngles.x, 0f);
}
#endregion
```

## 5. `PlayerMovement.cs`をPlayerにアタッチ
PlayerMovementのメンバにInspector上で渡すものは以下の写真の通りです！

![スクリーンショット 2022-12-03 13.24.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2609178/02183653-f464-fab3-1111-ad6abbcff815.png)

また、**MainCameraをPlayerの子オブジェクトとしてアタッチすることを忘れずに！**

# ソースコード

https://github.com/goooochi/Frank-Qiita-adventcalendar



# 参考になった記事
画面の分割（XMLタグがあることで、非常にわかりやすい記事でした）

https://qiita.com/flankids/items/0285577d278c88eaba87



# まとめ
メタバースの操作性について、バーチャルPayPayドームを参考に、比較的に直感的な操作の実装方法について書いてみました。ソースコードも載せておきましたので、ぜひ採用してみてください！

ここまで読んでいただき、ありがとうございました！