Система ввода в XR Interaction Toolkit 3.0 | How to XRI Input System Works

В этой статье рассматриваются способы работы с системой ввода в XR Interaction Toolkit.

Если хотите посмотреть ролик, то вот ссылки:🔗 https://youtu.be/zwDE4aYnxu4 альтернативная ссылка: 🔗 https://rutube.ru/video/8fe514d875b85a9ccd7588541f34feaa/

Открываем Unity Hub и создаем новый проект, в качестве шаблона будем использовать VR. Я использую версию Unity 6000.3.12f1.

На стандартной сцене у нас есть игрок, представленный как XR Origin Hands (XR Rig), переименуем его в Player для удобства.

Откроем стандартную папку с сэмплами (Samples -> XR Interaction Toolkit -> 3.4.1 -> Starter Assets), и здесь у нас есть XRI Default Input Actions, в котором у нас прописаны стандартные действия для обработки кнопок контроллеров. Мы не будем разбирать все действия, наша задача — понять, как их обрабатывать с помощью кода.

По запросу «XR Input unity» можем найти документацию, где расписаны различные инпуты для каждого поддерживаемого устройства.

Для примера, давайте создадим NPC, у меня есть шлем Oculus, и я хочу сделать так, чтобы диалог с NPC запускался тогда, когда игрок подходит в триггерную зону и нажимает кнопку B на контроллере. Текст с подсказкой, на какую кнопку необходимо нажать будет скрыт, также нам понадобится AudioSource для запуска озвучки диалога.

Создадим скрипт NPC, накинем его на NPC и откроем его в IDE.

Объявим необходимые объекты и методы:.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NPC : MonoBehaviour
{
    [SerializeField] private GameObject _hintText;
    private AudioSource _audioSource;
    

    private void Awake()
    {
        _audioSource = GetComponent<AudioSource>();
    }

    private void OnTriggerEnter(Collider other)
    {
        
    }

    private void OnTriggerExit(Collider other)
    {
        
    }
}

По умолчанию в дефолтных Actions нигде не прописана реакция на кнопку B, ее необходимо прописать самостоятельно. В Action Map XRI Right Interaction создадим новый Action и назовем его B Button.

В Action Type у нас стоит Button, поскольку нам необходимо считывать только нажатие на кнопку. Теперь найдем нужный Binding, для этого можно обратиться к документации, для окулуса, кнопка B запрограммирована на secondaryButton (B на правом контроллере, а Y на левом).

Выбираем XR Controller -> XR Controller (Right Hand) -> Usages -> SecondaryButton. Обязательно сохраните ассет, нажав кнопку Save Assets.

В скрипте создаем сериализуемое поле типа InputActionProperty, в которое мы передадим нашу кнопку. Не забудьте подключить соответствующий using.

Теперь в Unity можем назначить кнопку.

Вернемся к скрипту.

Теперь нам необходимо понять, нажата ли кнопка, это можно сделать несколькими способами, первый способ я никогда не использовал, но, он может быть полезен в тех случаях когда нам нужно отслеживать, зажата ли кнопка, например, в игре Assassins Creed Nexus для активации действий необходимо зажимать кнопку.

Нажмите на картинку, если гифка не запустилась

Давайте создадим корутину, назовем ее ListenBButtonClick, в ней запускаем бесконечный цикл while и проверяем, нажата ли кнопка, с помощью метода IsPressed() у BButton, внутри условия будет некая логика, например, мы будем запускать озвучку диалога и скрывать hintText. А если звук завершился, то будем показывать hintText.

private IEnumerator ListenBButtonClick()
    {
        while (true)
        {
            if (_bButton.action.IsPressed())
            {
                _audioSource.Play();
                _hintText.SetActive(false);
            }

            if (!_audioSource.isPlaying)
            {
                _hintText.SetActive(true);
            }

            yield return null;
        }
    }

Единственный нюанс заключается в том, что звук будет перезапускаться при каждом нажатии на кнопку, чтобы это исправить создадим логическую переменную _canSpeak, изначально она true и диалог будем запускать если нажата кнопка и можно говорить.

bool _canSpeak = true;
private IEnumerator ListenBButtonClick()
    {
        while (true)
        {
            if (_bButton.action.IsPressed() && _canSpeak)
            {
                _canSpeak = false;
                _audioSource.Play();
                _hintText.SetActive(false);
            }

            if (!_audioSource.isPlaying)
            {
                _canSpeak = true;
                _hintText.SetActive(true);
            }

            yield return null;
        }
    }

Запустим корутину в OnTriggerEnter() и остановим в OnTriggerExit().

    private void OnTriggerEnter(Collider other)
    {
        _hintText.SetActive(true);      
        StartCoroutine(ListenBButtonClick());
    }
    
    private void OnTriggerExit(Collider other)
    {
        _hintText.SetActive(false);
        StopAllCoroutines();
    }

Тестировать будем в симуляции, для этого нам нужно открыть Edit→Project Setting и на вкладке XR Interaction Toolkit импортировать XR Interaction Simulator:

Давайте запустим игру и кратко разберем, как работает симулятор. Используя WASD мы можем передвигаться, а зажав ПКМ – вращаться. Нажав Y откроется меню с подсказками по управлению симулятором. Чтобы активировать правую руку, необходимо нажать «]«. 

Теперь, нажав X мы можем увидеть Controller Actions в котором четко показано, что для нажатия кнопки B необходимо нажать кнопку 2 на клавиатуре.

В данном конкретном случае корректнее было бы отслеживать нажатие кнопки через подписку на событие. Уберем вызов и отмену корутины в методах OnTriggerEnter() и OnTriggerExit(), соответственно.

У любых кнопок есть поле action, у которого есть события: кнопка начала нажиматься (started), нажалась (performed), начала нажиматься но не до конца была отжата (canceled).

Нас интересует событие performed, ставим плюс равно и нажимаем Tab для автоматической генерации кода, называем этот метод BButtonPress, например.

Пропишем небольшую логику в BButtonPress:

    private void BButtonPress(InputAction.CallbackContext obj)
    {
        if (_canSpeak)
        {
            _canSpeak = false;
            _audioSource.Play();
            _hintText.SetActive(false);
        }
    }

Создадим новую корутину, которая будет отслеживать, закончился диалог или нет:

    private IEnumerator DialogueProcess()
    {
        while (true)
        {
            if (!_audioSource.isPlaying)
            {
                _canSpeak = true;
                _hintText.SetActive(true);
                yield return true;
            }
            yield return null;
        }
    }

Не забуем ее запустить

    private void BButtonPress(InputAction.CallbackContext obj)
    {
        if (_canSpeak)
        {
            _canSpeak = false;
            _audioSource.Play();
            _hintText.SetActive(false);

            StartCoroutine(DialogueProcess());
        }
    }

Также мы должны отписаться от события performed в OnTriggerExit():

    private void OnTriggerExit(Collider other)
    {
        _hintText.SetActive(false);
        _bButton.action.performed -= BButtonPress;
    }

Весь код скрипта NPC сейчас выглядит следующим образом:

using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;

public class NPC : MonoBehaviour
{
    [SerializeField] private InputActionProperty _bButton;
    [SerializeField] private GameObject _hintText;
    private AudioSource _audioSource;
    bool _canSpeak = true;


    private void Awake()
    {
        _audioSource = GetComponent<AudioSource>();
    }

    private void OnTriggerEnter(Collider other)
    {
        _hintText.SetActive(true);
        _bButton.action.performed += BButtonPress;
    }

    private void BButtonPress(InputAction.CallbackContext obj)
    {
        if (_canSpeak)
        {
            _canSpeak = false;
            _audioSource.Play();
            _hintText.SetActive(false);

            StartCoroutine(DialogueProcess());
        }
    }

    private void OnTriggerExit(Collider other)
    {
        _hintText.SetActive(false);
        _bButton.action.performed -= BButtonPress;
    }

    private IEnumerator DialogueProcess()
    {
        while (true)
        {
            if (!_audioSource.isPlaying)
            {
                _canSpeak = true;
                _hintText.SetActive(true);
                yield return true;
            }
            yield return null;
        }
    }
}

Итак, мы научились отслеживать клик по кнопке, подписываясь на события и используя метод IsPressed().

Теперь представьте, что мы создаем игру, в которой игрок будет представлять из себя мехатрона, и мы бы хотели управлять мехом, используя стики контроллера, т.е. нам необходимо знать как считывать значения со стика.

Для примера, в том же скрипте NPC уберем всё лишнее и создадим новый InputActionProperty, назовем его _rightStick. В Update()  у стика будем считывать значения, представленные типом Vector2.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class NPC : MonoBehaviour
{
    [SerializeField] InputActionProperty _rightStick;

    private void Update()
    {
        Debug.Log(_rightStick.action.ReadValue<Vector2>());
    }
}

Вернемся в движок и назначим соответствующий Action, в нашем случае это будет Thumbstick на правой руке.

Двойным кликом откроем его и рассмотрим повнимательнее. Он будет возвращать нам некое значение, представленное Vector 2, а кнопка контроллера представляет собой Primary2DAxis.

Запустим симуляцию. Выбираем правую руку. В консоли видим, что сейчас у нас по X и Y нулевые значения. Максимальное отведение стика влево (J) будет -1,0; вправо (L) — 1;0, вверх (I) и вниз (K), соответственно, 0;1 и 0;-1.

Естественно, в симуляции трудно отловить промежуточные значения, но если игру запустить в шлеме, то мы будем иметь доступ ко всему диапазону значений.

Если вам нужны более специфичные вещи не забывайте обращаться к документации.

Контакты

Если у вас есть какие-либо замечания или полезные дополнения к статье, можете написать мне в VK.

VK – https://vk.com/zabelyaevroman

Youtube – https://www.youtube.com/@vrfordev

Rutubehttps://rutube.ru/channel/38871248/