Customize the On-Screen Control to implement the D-pad

Page update date :
Page creation date :

Verification environment

Windows
  • Windows 11
Unity Editor
  • 2020.3.25f1
Input System Package
  • 1.2.0

Prerequisites for this tip

The following settings have been made in advance as a premise for the description of this tip.

You should also be familiar with the following tips:

On-Screen Control Stick is the standard orientation operation

The On-Screen Stick is implemented as a virtual stick, replicating the operation of a stick similar to that found on a physical controller. For example, if you want to move it to the right, you can first touch the screen to touch the stick, and then slide it to the right to knock the stick down.

It is easy to imagine as a stick operation, but on the contrary, when you want to move it to the right immediately, (1) touch, (2) slide to the right, Since it requires a two-step operation, the response will inevitably be a little delayed. This is especially true when you need to make a quick change of direction in a row.

D-pad that determines direction at the moment of touch (non-standard)

The best input method for quick direction input is one that can determine the direction at the moment of touching by placing something like a D-pad or arrow key. It's not made by Unity, but in my game Little Saber, I place the arrow keys so that you can quickly move in the direction you touch. Of course, you can also slide up or left while touching to switch movements.

However, the On-Screen Control standard is not easy to place and implement because there are only On-Screen Sticks and On-Screen Buttons.

You can also create a pseudo-D-pad by arranging four buttons as shown in the figure below, but it is inconvenient because you cannot input diagonally. If you place 8 buttons, diagonal operation is possible, but flow direction operation such as "← ↙ ↓" is still not possible.

Create a directional pad with On-Screen Control customization

As you can see, On-Screen Control only comes standard with Stick and Button, but you can customize the missing features with your own scripts. So here I would like to create a directional pad with On-Screen Control customization.

Action Map

We will use the action map, but we will use the one created in the previous tips as it is, so the procedure is omitted.

You've also written some code.

Positioning Objects

Place a text area to display your input and a button that replaces the D-pad. In this case, the buttons are arranged, but you can replace them with an image that is easier to understand.

Script to display input

Because On-Screen Control replaces touch with physical controller interaction, Create a script that displays your input in text as the action map works.

The content is the same as before, so I will omit the explanation.

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}";
  }
}

After setting it up, first check if it works with your keyboard or gamepad.

Customizing On-Screen Controls

This brings us to the main topic of these tips. Customizing the On-Screen Control is a script, so first create a script. The name is arbitrary, but in this case OnScreenDpad it is .

The script looks like this:

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);
  }
}

You create an OnScreenControl On-Screen Control customization class by inheriting from the Customize class. In addition, to receive various touch events, inherit the target interface. Here, "when touching", "when moving while touching", "when the touch is released", and "before dragging" are processed, so the following interfaces are also described respectively.

public class OnScreenDpad
  : OnScreenControl, IPointerDownHandler, IPointerUpHandler, IDragHandler, IInitializePotentialDragHandler
Interface Contents
IPointerDownHandler When touched
IPointerUpHandler When you release the touch
IDragHandler When you move while touching
IInitializePotentialDragHandler Before you start dragging

The following fields are declared:

controlPathInternalOnScreenControl is required if you inherit from a class. It holds a string of which button of the input device to map to, but basically you can write it as it is. InputControl However, the attribute should contain the value to be retained (Vector2 in this case).

_objectPosition_objectSizeHalf and hold half the position and size of the button in advance and use it later for calculations.

[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;

The method, called Start after the object is initialized, gets half the position and size of the button on the canvas. The size also takes into account the scale. Start It is processed by a method, but if it can be used correctly in the calculation in the end, the acquisition timing can be anywhere.

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

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

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

OnInitializePotentialDrag is called when you want to start dragging after touch. While dragging, OnDrag the method is called. A threshold is set to prevent drag judgment to some extent so that the finger does not move a little and the drag judgment does not occur due to the operation of simply touching.

This time, since it is an input that assumes that you want to perform a fine-tuning operation, disable this threshold setting and immediately make a drag judgment. eventData.useDragThreshold false You can override the threshold by setting to .

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

Below are the events called when touching, dragging, and releasing respectively. OnPointerDown OnDrag Since and , each performs Operate the same input processing, so we create a separate method and call it. OnPointerUp Now, pass it to the method to SendValueToControl Vector2.zero notify the control that it has stopped typing.

/// <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 The method is the core D-pad input operation.

First of all, the touch position can be obtained with , but since this coordinate is the coordinate of the eventData.position game screen, RectTransformUtility.ScreenPointToLocalPointInRectangle method to convert to canvas coordinates. The values to be passed are "canvas", " RectTransformtouch position", "camera", and "receiving variable".

After getting the "touch position on the canvas", convert it to the position of the button as the origin. Just subtract the position () of the_objectPosition object.

Next, since the touch position is still a number on the canvas, convert this value to a ratio of 0~1. If you divide by half the size of the button, the touch position within the range of the button will be 0~1. If you drag with touch, the value will be 1 or more because it accepts operations even outside the button. Vector2.ClampMagnitude Do this in the method so that it does not exceed 1.

Finally, pass the input value SendValueToControl converted to 0~1 to the method. The On-Screen Control does the rest for you.

/// <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);
}

Attach the script you created to the button. This action is set to be treated as the operation of the left stick of the Gamepad.

Try moving the game and touch the buttons. I think you can see that the value obtained changes depending on the touch position. Also, you can see that even if you drag while touching, the value changes depending on the drag position.