On-Screen Control을 사용자 정의하여 D-패드 구현

페이지 업데이트 :
페이지 생성 날짜 :

검증 환경

윈도우
  • 윈도우 11
Unity 에디터
  • 2020.3.25f1
입력 시스템 패키지
  • 1.2.0

이 팁의 전제 조건

이 팁에 대한 설명의 전제로 다음 설정이 미리 이루어졌습니다.

또한 다음 팁을 숙지해야 합니다.

On-Screen Control Stick은 표준 방향 조작입니다

On-Screen Stick은 가상 스틱으로 구현되어 물리적 컨트롤러에서 볼 수 있는 것과 유사한 스틱의 작동을 복제합니다. 예를 들어 오른쪽으로 이동하려면 먼저 화면을 터치하여 스틱을 터치한 다음 오른쪽으로 밀어 스틱을 쓰러뜨릴 수 있습니다.

스틱 조작으로 상상하기 쉽지만, 반대로 곧바로 오른쪽으로 이동하고 싶을 때는, (1) 터치, (2) 오른쪽으로 밀고, 2단계 작업이 필요하기 때문에 응답이 약간 지연될 수밖에 없습니다. 연속으로 방향을 빠르게 변경해야 할 때 특히 그렇습니다.

터치하는 순간 방향을 결정하는 방향 패드(비표준)

빠른 방향 입력을 위한 가장 좋은 입력 방법은 방향 패드나 화살표 키와 같은 것을 배치하여 터치하는 순간의 방향을 결정할 수 있는 방법입니다. 유니티에서 만든 것은 아니지만, 제 게임 리틀 세이버에서는 터치하는 방향으로 빠르게 이동할 수 있도록 화살표 키를 배치합니다. 물론, 터치하는 동안 위나 왼쪽으로 밀어서 움직임을 전환하는 것도 가능합니다.

그러나 On-Screen Control 표준은 On-Screen Stick과 On-Screen Buttons만 있기 때문에 배치 및 구현이 쉽지 않습니다.

아래 그림과 같이 4개의 버튼을 배열하여 의사 D패드를 만들 수도 있지만 대각선으로 입력할 수 없어 불편합니다. 8 개의 버튼을 배치하면 대각선 조작은 가능하지만 "← ↙ ↓"와 같은 흐름 방향 조작은 여전히 불가능합니다.

On-Screen Control 사용자 지정으로 방향 패드 만들기

보시다시피 On-Screen Control은 Stick 및 Button에서만 표준으로 제공되지만 자체 스크립트로 누락된 기능을 사용자 지정할 수 있습니다. 그래서 여기에서는 On-Screen Control 사용자 정의로 방향 패드를 만들고 싶습니다.

작업 맵

액션 맵을 사용하되 이전 팁에서 만든 맵을 그대로 사용하므로 절차는 생략합니다.

또한 몇 가지 코드를 작성했습니다.

위치 지정 개체

입력을 표시할 텍스트 영역과 방향 패드를 대체하는 버튼을 배치합니다. 이 경우 버튼이 정렬되어 있지만 이해하기 쉬운 이미지로 바꿀 수 있습니다.

입력을 표시하는 스크립트

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 컨트롤을 사용자 지정하는 것은 스크립트이므로 먼저 스크립트를 만듭니다. 이름은 임의적이지만 이 경우에는 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);
  }
}

OnScreenControl On-Screen Control 사용자 지정 클래스는 Customize 클래스에서 상속하여 만듭니다. 또한 다양한 터치 이벤트를 수신하려면 대상 인터페이스를 상속합니다. 여기서는 "터치할 때", "터치하면서 이동할 때", "터치를 놓았을 때", "드래그하기 전에"가 처리되므로, 이하의 인터페이스도 각각 설명한다.

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.useDragThreshold false 로 설정하여 임계값을 재정의할 수 있습니다.

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

아래는 터치, 드래그 및 놓을 때 각각 호출되는 이벤트입니다. OnPointerDown OnDrag Since 및 , 각각은 동일한 입력 처리를 수행 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 이 방법은 코어 D-패드 입력 작업입니다.

우선, 터치 위치는 로 얻을 수 있지만, 이 좌표는 게임 화면의 좌표 eventData.position 이기 때문에, RectTransformUtility.ScreenPointToLocalPointInRectangle 메서드를 사용하여 캔버스 좌표로 변환할 수 있습니다. 전달할 값은 "canvas", "touch position", "camera" 및 "receiving variable" 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);
}

만든 스크립트를 단추에 첨부합니다. 이 동작은 게임패드의 왼쪽 스틱 조작으로 처리되도록 설정되어 있습니다.

게임을 움직여 버튼을 터치해 보세요. 터치 위치에 따라 얻어지는 값이 달라지는 것을 알 수 있다고 생각합니다. 또한 터치하면서 드래그해도 드래그 위치에 따라 값이 달라지는 것을 볼 수 있습니다.