U
タブ 03カメラと入力6 レッスン — タップで開く
05 / 06 · 10 分
カメラと入力

タッチ操作とジョイスティック

ここまでのプロジェクト

Assets/

Scenes/

Arena.unity

TouchCanvas

Scripts/

InputRouter.csVirtualJoystick.csSafeAreaFitter.cs

Settings/

PlayerControls.inputactions

すでにあるものこのレッスンで追加

前回のレッスンで入力をすべて InputRouter に通したので、今回はゲームプレイ側のスクリプトを一文字も触らなくて済みます。Knight はすでに、Move アクションを埋められるものなら何でも入力として受け取れます。

タッチ操作は別のゲームではありません。キーボードと同じ Vector2 を生み出す、UI レイヤーの一つです。

アニメーション図

親指でノブを引くと、ノブが方向になり、その方向が同じ Move アクションに流れ込みます。

一番速い道:On-Screen Controls

  1. 1

    UI の Canvas を作成します:GameObject▸UI▸Canvas で TouchCanvas と名付け、Render Mode は Screen Space Overlay に設定してください。

  2. 2

    Canvas Scaler は Scale With Screen Size、リファレンス解像度 1080×1920、Match 0.5 にします。これが無いと、一部のスマホで UI が極端に小さくなります。

  3. 3

    スティックの土台用に Image を置き、その子としてノブ用の Image を追加します。

  4. 4

    ノブに On-Screen Stick コンポーネントを付け、Control Path には Move のバインディングを指定します。

  5. 5

    アクションボタンにも同じ手順で On-Screen Button を追加してください。Input System からは仮想ゲームパッドとして見えます。

自分のスティックを実装する

自前のスティックなら、指についてくる原点、デッドゾーン、独自の手触りを好きなように設計できます。コードは 40 行ほどです。

VirtualJoystick.cs — put on the stick base Image
using UnityEngine;
using UnityEngine.EventSystems;

public class VirtualJoystick : MonoBehaviour,
    IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    [SerializeField] private RectTransform knob;
    [SerializeField] private float radius = 90f;
    [SerializeField] private float deadZone = 0.15f;

    private RectTransform baseRect;
    public Vector2 Value { get; private set; }

    void Awake() => baseRect = GetComponent<RectTransform>();

    public void OnPointerDown(PointerEventData e) => OnDrag(e);

    public void OnDrag(PointerEventData e)
    {
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            baseRect, e.position, e.pressEventCamera, out Vector2 local);

        Vector2 clamped = Vector2.ClampMagnitude(local, radius);
        knob.anchoredPosition = clamped;

        Vector2 raw = clamped / radius;                 // -1 .. 1
        Value = raw.magnitude < deadZone ? Vector2.zero : raw;
    }

    public void OnPointerUp(PointerEventData e)
    {
        knob.anchoredPosition = Vector2.zero;
        Value = Vector2.zero;                           // always reset
    }
}
  • ScreenPointToLocalPointInRectangle を使うと、画面解像度に関係なく、指の位置をスティック内部の座標に変換できます。

  • ClampMagnitude を使えばノブは正方形ではなく円の中に収まります。四角いスティックは、触った瞬間に分かるくらい違和感があります。

  • デッドゾーンを 0.15 にしておけば、親指を画面に添えているだけでキャラクターが漂ってしまうのを防げます。

タップとドラッグ、右半分はカメラ用

C# スクリプト
void Update()
{
    // right half of the screen rotates the camera
    foreach (Touch t in Input.touches)
    {
        if (t.position.x < Screen.width * 0.5f) continue;   // left half is the stick
        if (t.phase != TouchPhase.Moved) continue;

        yaw   += t.deltaPosition.x * touchSensitivity;
        pitch -= t.deltaPosition.y * touchSensitivity;
    }
}

モバイル UI で省略できない決まり

  • タップ領域は iOS なら 44 pt、Android なら 48 dp 以上、およそ 7 から 9 mm は確保してください。これより小さいとプレイヤーは押し損ないを繰り返します。

  • 画面下の隅には小さなボタンを置かないでください。親指が休まる場所であり、システムのジェスチャーも通る場所です。

  • UI は中央ではなく角へアンカーし、そのうえでノッチを避けるために Screen.safeArea に従ってください。

  • ビルドする前に、Game ビューのドロップダウン → Simulator から実機のプロファイルを指定して確かめておきます。

SafeAreaFitter.cs — put on a full-screen panel
using UnityEngine;

[RequireComponent(typeof(RectTransform))]
public class SafeAreaFitter : MonoBehaviour
{
    void Start()
    {
        RectTransform rt = GetComponent<RectTransform>();
        Rect safe = Screen.safeArea;

        Vector2 min = safe.position;
        Vector2 max = safe.position + safe.size;
        min.x /= Screen.width;  min.y /= Screen.height;
        max.x /= Screen.width;  max.y /= Screen.height;

        rt.anchorMin = min;
        rt.anchorMax = max;
        rt.offsetMin = rt.offsetMax = Vector2.zero;
    }
}