On-Screen Control をカスタマイズして方向パッドを実装する

ページ更新日 :
ページ作成日 :

検証環境

Windows
  • Windows 11
Unity エディター
  • 2020.3.25f1
入力システムパッケージ
  • 1.2.0

この Tips の前提設定

この Tips の説明の前提として以下の設定を事前に行っています。

また、以下の Tips の内容を理解しているものとします。

On-Screen Control 標準の方向決定操作であるスティック

On-Screen Stick は仮想的なスティックとして実装されており、物理コントローラーにあるスティック操作と似た操作を再現しています。 例えば右に動かしたい場合はまず、①画面をタッチしてスティックを触る操作を行い、②その後右にスライドさせてスティックを倒す、という操作になります。

スティックの操作としてはイメージしやすいですが、逆に即座に右に移動させたいときに、①タッチする、②右にスライドする、 という2段階の操作が必要になるためどうしても反応が少し遅れてしまいます。連続して素早く方向転換する必要がある場合なんかは顕著です。

タッチした瞬間に方向を決定する方向パッド (非標準搭載)

方向入力を素早く行う入力方式として十字キーや方向キーのようなものを配置してタッチした瞬間に方向を決定できるものがベストです。 Unity 製ではないですが、私が制作したゲーム「リトルセイバー」では方向キーを配置してタッチした方向にすぐに移動できるようにしています。 もちろんタッチしたまま上や左にスライドして移動の切り替えをすることも可能です。

しかし On-Screen Control の標準には On-Screen Stick と On-Screen Button しかないため簡単に配置して実現することはできません。

下の図のようにボタンを4つ配置して疑似的に十字キーを作成することもできますが、斜め入力ができないので不便です。 ボタンを8つ配置すれば斜め操作も可能ですが「←↙↓」のような流れる方向操作はやはりできません。

On-Screen Control のカスタマイズで方向パッドを作成する

順術の通り On-Screen Control には Stick と Button しか標準でありませんが、無い機能については独自にスクリプトでカスタマイズすることが可能です。 なのでここでは On-Screen Control のカスタマイズで方向パッドを作成してみたいと思います。

アクションマップ

アクションマップを使いますが前 Tips で作成したものをそのまま使いますので手順は割愛します。

コードも作成しておきます。

オブジェクトの配置

入力内容を表示するためのテキストエリアと方向パッドの変わりとなるボタンを配置します。 今回はボタンを配置していますが、分かりやすい画像に置き換えても構いません。

入力内容を表示するスクリプト

On-Screen Control はタッチ操作を物理コントローラーの操作に置き換えるので、 アクションマップの動作に合わせて入力内容をテキストに表示するスクリプトを作成します。

内容は以前のものと変わりませんので説明は省きます。

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;

public class InputActionScript : MonoBehaviour
{
  /// <summary>情報を表示させるテキストオブジェクト。</summary>
  [SerializeField] private Text TextObject;

  /// <summary>アクションマップから自動生成されたクラス。</summary>
  private InputActionSample _actionMap;

  private void Awake()
  {
    // 各操作を行ったときに呼ばれるイベントを設定する
    _actionMap = new InputActionSample();
    _actionMap.Action2D.Move.performed += context => OnMove(context);
    _actionMap.Action2D.Attack.performed += context => OnAttack(context);
    _actionMap.Action2D.Move.canceled += context => OnMove(context);
    _actionMap.Action2D.Attack.canceled += context => OnAttack(context);
  }

  private void OnEnable()
  {
    // このオブジェクトが有効になったときにアクションマップを有効にする
    _actionMap.Enable();
  }

  private void OnDisable()
  {
    // このオブジェクトが無効になったときにアクションマップが余計な動作を起こさないように無効にする
    _actionMap.Disable();
  }

  /// <summary>
  /// Move 操作をした時に呼ばれるメソッドです。
  /// </summary>
  /// <param name="context">コールバックパラメータ。</param>
  public void OnMove(InputAction.CallbackContext context)
  {
    // Move の入力量を取得
    var vec = context.ReadValue<Vector2>();
    TextObject.text = $"Move:({vec.x:f2}, {vec.y:f2})\n{TextObject.text}";
  }

  /// <summary>
  /// Attack 操作をした時に呼ばれるメソッドです。
  /// </summary>
  /// <param name="context">コールバックパラメータ。</param>
  public void OnAttack(InputAction.CallbackContext context)
  {
    // Attack ボタンの状態を取得
    var value = context.ReadValueAsButton();
    TextObject.text = $"Attack:{value}\n{TextObject.text}";
  }
}

設定したらまずはキーボードやゲームパッドで動作するか確認してください。

On-Screen Control のカスタマイズ

ここからが本 Tips の本題となります。 On-Screen Control のカスタマイズはスクリプトになるのでまずはスクリプトを作成します。 名前は任意ですがここでは OnScreenDpad とします。

スクリプトは以下のようにします。

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.OnScreen;

public class OnScreenDpad
  : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler, IInitializePotentialDragHandler
{
  [InputControl(layout = "Vector2")]
  [SerializeField]
  private string _controlPath;
  /// <summary><see cref="OnScreenControl"/> で定義された値。</summary>
  protected override string controlPathInternal { get => _controlPath; set => _controlPath = value; }

  /// <summary>オブジェクトの位置。</summary>
  private Vector2 _objectPosition;

  /// <summary>オブジェクトのサイズの半分 (スケールも含む)。</summary>
  private Vector2 _objectSizeHalf;


  /// <summary>
  /// オブジェクトが動作する最初のタイミングで1回だけ呼ばれます。
  /// </summary>
  private void Start()
  {
    var rectTransform = (RectTransform)base.transform;

    // オブジェクトの位置を取得
    _objectPosition = rectTransform.anchoredPosition;

    // オブジェクトのサイズの半分を取得 (スケールサイズも考慮)
    _objectSizeHalf = rectTransform.sizeDelta * rectTransform.localScale / 2f;
  }

  /// <summary>ドラッグの初期化処理として呼ばれます。</summary>
  /// <param name="eventData">タッチ情報。</param>
  public void OnInitializePotentialDrag(PointerEventData eventData)
  {
    // タッチのスライド操作を即座に発生させたいのでドラッグ開始までの閾値を無効にします
    eventData.useDragThreshold = false;
  }

  /// <summary>タッチしたタイミングで呼ばれます。</summary>
  /// <param name="eventData">タッチ情報。</param>
  public void OnPointerDown(PointerEventData eventData)
  {
    Operate(eventData);
  }

  /// <summary>タッチした後ドラッグするたびに呼ばれます。</summary>
  /// <param name="eventData">タッチ情報。</param>
  public void OnDrag(PointerEventData eventData)
  {
    Operate(eventData);
  }

  /// <summary>タッチを離したときに呼ばれます。</summary>
  /// <param name="eventData">タッチ情報。</param>
  public void OnPointerUp(PointerEventData eventData)
  {
    // 入力をやめた扱いにしたいので zero を渡します
    SendValueToControl(Vector2.zero);
  }

  /// <summary>
  /// 方向パッドの入力処理を行います。
  /// </summary>
  /// <param name="eventData">タッチ情報。</param>
  private void Operate(PointerEventData eventData)
  {
    // タッチ位置を Canvas 上の位置に変換します
    RectTransformUtility.ScreenPointToLocalPointInRectangle(
      transform.parent.GetComponentInParent<RectTransform>(),
      eventData.position,
      eventData.pressEventCamera,
      out Vector2 localPoint);

    // Dpad の中心を原点としたタッチ位置
    Vector2 positionInDpad = localPoint - _objectPosition;

    // タッチ位置をオブジェクトサイズの半分で割り 0~1 の範囲に収めます
    Vector2 positionRate = Vector2.ClampMagnitude(positionInDpad / _objectSizeHalf, 1);

    // 入力値を OnScreenControl に渡してその後の処理を任せます。
    SendValueToControl(positionRate);
  }
}

On-Screen Control のカスタマイズクラスは OnScreenControl クラスを継承して作成します。 また、各種タッチイベントを受け取るには対象のインターフェースを継承します。 ここでは「タッチしたとき」「タッチしたまま動かしたとき」「タッチを離したとき」と「ドラッグ開始前」を処理するのでそれぞれ以下のインターフェースも記述します。

public class OnScreenDpad
  : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler, IInitializePotentialDragHandler
インターフェース 内容
IPointerDownHandler タッチしたとき
IPointerUpHandler タッチを離したとき
IDragHandler タッチしたまま動かしたとき
IInitializePotentialDragHandler ドラッグ開始前

以下のフィールドを宣言しています。

controlPathInternalOnScreenControl クラスを継承した場合に必須です。 入力デバイスのどのボタンとマッピングするかの文字列を保持しますが、基本的にはこのまま記述して良いです。 ただし InputControl 属性には保持する値(ここでは Vector2)を記述してください。

_objectPosition_objectSizeHalf はあらかじめボタンの位置とサイズの半分を保持しておき後で計算に使います。

[InputControl(layout = "Vector2")]
[SerializeField]
private string _controlPath;
/// <summary><see cref="OnScreenControl"/> で定義された値。</summary>
protected override string controlPathInternal { get => _controlPath; set => _controlPath = value; }

/// <summary>オブジェクトの位置。</summary>
private Vector2 _objectPosition;

/// <summary>オブジェクトのサイズの半分 (スケールも含む)。</summary>
private Vector2 _objectSizeHalf;

オブジェクトが初期化された後に呼ばれる Start メソッドではボタンのキャンバス上の位置とサイズの半分を取得しています。 サイズはスケールも考慮しています。 Start メソッドで処理していますが、最終的に計算で正しく使えるなら取得タイミングはどこでもいいです。

// <summary>
// オブジェクトが動作する最初のタイミングで1回だけ呼ばれます。
// </summary>
private void Start()
{
  var rectTransform = (RectTransform)base.transform;

  // オブジェクトの位置を取得
  _objectPosition = rectTransform.anchoredPosition;

  // オブジェクトのサイズの半分を取得 (スケールサイズも考慮)
  _objectSizeHalf = rectTransform.sizeDelta * rectTransform.localScale / 2f;
}

OnInitializePotentialDrag はタッチ後ドラッグを開始するタイミングで呼ばれます。 ドラッグ中は OnDrag メソッドが呼ばれますが、 単にタッチしただけの操作でちょっと指が動いてしまいドラッグ判定になってしまわないようにある程度ドラッグ判定されないための閾値が設定されています。

今回は微調整の操作を行う前提の入力なのでこの閾値の設定を無効にしてすぐにドラッグ判定になるようにします。 eventData.useDragThresholdfalse にすることによって閾値を無効にできます。

/// <summary>ドラッグの初期化処理として呼ばれます。</summary>
/// <param name="eventData">タッチ情報。</param>
public void OnInitializePotentialDrag(PointerEventData eventData)
{
  // タッチのスライド操作を即座に発生させたいのでドラッグ開始までの閾値を無効にします
  eventData.useDragThreshold = false;
}

以下はそれぞれタッチしたとき、ドラッグしているとき、離したときに呼ばれるイベントです。 OnPointerDownOnDrag はそれぞれ同じ入力処理をするので Operate というメソッドを別途作成しそれを呼ぶようにしています。 OnPointerUp では入力を止めたことをコントロールに通知するように SendValueToControl メソッドに Vector2.zero を渡します。

/// <summary>タッチしたタイミングで呼ばれます。</summary>
/// <param name="eventData">タッチ情報。</param>
public void OnPointerDown(PointerEventData eventData)
{
  Operate(eventData);
}

/// <summary>タッチした後ドラッグするたびに呼ばれます。</summary>
/// <param name="eventData">タッチ情報。</param>
public void OnDrag(PointerEventData eventData)
{
  Operate(eventData);
}

/// <summary>タッチを離したときに呼ばれます。</summary>
/// <param name="eventData">タッチ情報。</param>
public void OnPointerUp(PointerEventData eventData)
{
  // 入力をやめた扱いにしたいので zero を渡します
  SendValueToControl(Vector2.zero);
}

Operate メソッドが核となる方向パッドの入力操作となります。

まずタッチ位置は eventData.position で取得できますが、この座標はゲーム画面の座標なので RectTransformUtility.ScreenPointToLocalPointInRectangle メソッドを使用してキャンバスの座標に変換します。 それぞれ渡す値としては「キャンバスの RectTransform」「タッチ位置」「カメラ」「受け取る変数」となります。

「キャンバス上のタッチ位置」を取得したらボタンの位置を原点とした位置に変換します。 オブジェクトの位置(_objectPosition)を引くだけです。

次にタッチ位置がまだキャンバス上の数値なのでこの値を 0~1 の割合になるように変換します。 ボタンの半分のサイズで割ればボタンの範囲内のタッチ位置が 0~1 になりますが、 タッチでドラッグした場合ボタン外でも操作を受け付けるため 1 以上の値になってしまいます。 これを Vector2.ClampMagnitude メソッドで 1 を超えないようにします。

最後に 0~1 に変換した入力値を SendValueToControl メソッドに渡します。 後は On-Screen Control が必要な処理を行ってくれます。

/// <summary>
/// 方向パッドの入力処理を行います。
/// </summary>
/// <param name="eventData">タッチ情報。</param>
private void Operate(PointerEventData eventData)
{
  // タッチ位置を Canvas 上の位置に変換します
  RectTransformUtility.ScreenPointToLocalPointInRectangle(
    transform.parent.GetComponentInParent<RectTransform>(),
    eventData.position,
    eventData.pressEventCamera,
    out Vector2 localPoint);

  // Dpad の中心を原点としたタッチ位置
  Vector2 positionInDpad = localPoint - _objectPosition;

  // タッチ位置をオブジェクトサイズの半分で割り 0~1 の範囲に収めます
  Vector2 positionRate = Vector2.ClampMagnitude(positionInDpad / _objectSizeHalf, 1);

  // 入力値を OnScreenControl に渡してその後の処理を任せます。
  SendValueToControl(positionRate);
}

作成したスクリプトはボタンにアタッチしてください。 今回の操作は Gamepad の左スティックの操作として扱われるように設定します。

ゲームを動かしボタンをタッチ操作してみてください。 タッチ位置によって取得される値が変わることを確認できると思います。 また、タッチしたままドラッグしてもドラッグ位置によって値が変化することも確認できると思います。