Продолжим серию статей про OpenXR. В конце концов получим контроллер игрока, обладающий базовыми навыками — перемещением, поворотом и взаимодействием с объектами. Взаимодействие с объектами мы рассмотрим в следующей статье. В этой же мы сделаем телепортацию игрока и его поворот.

Для работы над текущей задачей мы возьмем проект из предыдущего урока за основу, удалив из него скрипт HandCapsule. Это был демонстрационный скрипт, показывающий нам как работать с API OpenXR из Unity, и более он нам не потребуется — мы будем писать уже "по-настоящему".

Перед тем как начать, давайте отделим неподконтрольную нам часть от подконтрольной. Создадим дочерний для XR Rig объект Player Avatar, а в нём создадим объекты Left Hand и Right Hand. Рукам-капсулам мы зададим scale (0.05, 0.05, 0.05). В руках мы создадим объект Pointer и разместим его на "макушке" наших капсул и повернем его таким образом, чтобы forward Pointer'а смотрел наверх. Значения трансформа Pointer'а: поворот по x -90, координаты (0, 1, 0).

Hand

Создадим класс Hand — основной скрипт для всех работы с инпутом контроллеров и взаимодействия с виртуальным миром. В начале класса мы объявим поля TargetTransform, чтобы понимать, какое местоположение сейчас у "настоящего" контроллера, InputDeviceManager из прошлой статьи, чтобы взаимодействовать с инпутом, и флаг IsLeftHand, чтобы понимать, какой InputDevice нам брать для обработки инпутов. Также, заведем свойство InputDevice, возвращающее конкретный для данной руки InputDevice из InputDeviceManager.

  public Transform TargetTransform;
  public InputDeviceManager InputDeviceManager;
  public bool IsLeftHand;

  public InputDevice InputDevice => IsLeftHand ? InputDeviceManager.LeftController : InputDeviceManager.RightController;

Данный скрипт навесим на оба объекта рук: Left Hand и Right Hand, и проставим им все необходимые ссылки, а флаг IsLeftHand выставим в true для Left Hand.

Компонент Hand для левой руки
Компонент Hand для левой руки

Перед началом работы с инпутом, необходимо повторить функционал из предыдущего урока: перемещение капсул за руками. В прошлом уроке мы просто повесили капсулы на Tracked Pose, сейчас же объекты капсул существуют без какой-либо логики перемещения в пространстве. Напишем её, благо она очень простая: просто будем повторять в LateUpdate положение и вращение соответствующей Tracked Pose.

private void LateUpdate()
{
    transform.position = TargetTransform.position;
    transform.rotation = TargetTransform.rotation;
}

Hand будет выполнять на данном этапе роль обработчика событий с инпута соответствующего ему контроллера, потому давайте заведем четыре события, необходимые нам для реализации телепортации игрока и его поворота: TeleportPressed, TeleportReleased, TurnLeft и TurnRight. Делегат для них будем использовать void HandInteraction(Hand hand) — его я положил в отдельный файл, но в скрипте приведу рядом с событями.

public delegate void HandInteraction(Hand hand);

public event HandInteraction TeleportPressed;
public event HandInteraction TeleportReleased;

public event HandInteraction TurnLeft;
public event HandInteraction TurnRight;

Немного о событиях:

  • TeleportPressed будем вызывать тогда, когда стик только отклонился вперёд.

  • TeleportReleased будем вызывать тогда, когда стик перешел из состояния "отклонен вперед" в нейтральное.

  • TurnLeft и TurnRight будем вызывать тогда, когда стик отклонился влево или вправо соответственно.

Реализуем обработку данных событий:

private void Update()
{
    ProcessThumbstick();
}

private void ProcessThumbstick()
{
    _prevThumbstickAxis = _thumbstickAxis;
    InputDevice.TryGetFeatureValue(CommonUsages.primary2DAxis, out _thumbstickAxis);
    
    const float teleportThreshold = 0.66f;
    if (_prevThumbstickAxis.y < teleportThreshold && _thumbstickAxis.y >= teleportThreshold)
    {
        TeleportPressed?.Invoke(this);
    }
    else if (_prevThumbstickAxis.y >= teleportThreshold && _thumbstickAxis.y < teleportThreshold)
    {
        TeleportReleased?.Invoke(this);
    }
    
    const float turnThreshold = 0.66f;
    if (_prevThumbstickAxis.x < turnThreshold && _thumbstickAxis.x >= turnThreshold)
    {
        TurnRight?.Invoke(this);
    }
    else if (_prevThumbstickAxis.x >= -turnThreshold && _thumbstickAxis.x < -turnThreshold)
    {
        TurnLeft?.Invoke(this);
    }
}

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

Игрок

Реализуем два простых метода на перемещение игрока и его поворот. Создадим класс Player и опишем методы для работы с Transform XR Rig'а:

public class Player : MonoBehaviour
{
    public void SetPosition(Vector3 position)
    {
        transform.position = position;
    }

    public void Rotate(float angle)
    {
        transform.Rotate(0, angle, 0);
    }
}

Сам компонент навешиваем на объект XR Rig. Вызов метод SetPosition переместит игрока в указанные координаты, а метод Rotate будет отвечать за поворачивание игрока по оси Y (в горизонтальной плоскости).

Телепортация

Реализация телепортации состоит из двух скриптов: Teleport.cs, реализующий логику телепортации и который мы напишем сами, и Arc.cs, занимающийся отрисовкой арки телепортации и который мы возьмем с gist.github.com Скрипт рисования арки является косметическим и вместо него можно было бы использовать простой LineRenderer, но я выбираю красивое и готовое решение для концентрации на логике работы с OpenXR.

Перейдем к самой логике телепорта.

Создадим новый Mono Behaviour и называем его Teleport. В нём вводим несколько публичных полей: ссылку на игрока, ссылку на руку для подписки на события стика, ссылка на арку для управления её отрисовкой, ссылка на точку, из которой будет рисоваться арка, и скорость луча (влияет на его дистанцию).

public Player Player;
public Hand Hand;
public Arc Arc;
public Transform Source;
public float TeleportVelocity = 10f;

Также, введём несколько приватных переменных: _arcEnabled для оптимизации работы с просчётом луча, _arcIsValid для присваивания результата просчёта луча и _lastArcTargetPosition для хранения последних координат, куда утыкался луч телепорта.

private bool _arcEnabled;
private bool _arcIsValid;
private RaycastHit _lastArcTargetPosition;

Далее, подпишемся на события Hand.TeleportPressed и Hand.TeleportReleased и напишем логику для них. На отведение стика мы будем включать луч и начинать его отрисовывать, а как только стик будет возвращаться в своё исходное положение, мы будем выключать отрисовку луча и телепортировать игрока в конечные координаты, если телепортировать туда было возможным.

private void OnEnable()
{
    Hand.TeleportPressed += StartTeleport;
    Hand.TeleportReleased += FinishTeleport;
}

private void OnDisable()
{
    Hand.TeleportPressed -= StartTeleport;
    Hand.TeleportReleased -= FinishTeleport;
}

private void StartTeleport(Hand hand)
{
    _arcEnabled = true;
    Arc.Show();
}

private void FinishTeleport(Hand hand)
{
    _arcEnabled = false;
    if (_arcIsValid && Arc.IsArcValid())
    {
        Player.SetPosition(_lastArcTargetPosition.point);
    }
    Arc.Hide();
}

Также напишем логику рисования луча. Скрипт отрисовки арки требует для отрисовки передачи каждый кадр местоположения, вращения и других параметров работы луча (SetArcData) и вызова самого метода отрисовки (DrawArc). Будем вызывать их в Update.

private void Update()
{
    if (_arcEnabled)
    {
        Arc.SetArcData(Source.position, TeleportVelocity * Source.forward, true, false, false);
        _arcIsValid = Arc.DrawArc(out _lastArcTargetPosition);
    }
}

Навесим оба скрипта (Arc и Teleport) на объекты Left Hand и Right Hand и настроим их.

Пример настройки компонентов Arc и Teleport на примере левой руки. Материал для телепорта можно использовать стандартный.
Пример настройки компонентов Arc и Teleport на примере левой руки. Материал для телепорта можно использовать стандартный.

Поворот игрока

Создадим класс TurnPlayer — класс, ответственный за поворот игрока при отклонении стика влево и вправо.

Объявим необходимые поля для работы скрипта: ссылка на игрока, ссылка на руку и угол поворота игрока при отклонении стика.

public Player Player;
public Hand Hand;
public float Angle = 45f;

И опишем методы подписки на события Hand.TurnLeft и Hand.TurnRight, поворачивающие игрока на указанное в поле Angle значении:

private void OnEnable()
{
    Hand.TurnLeft += TurnLeft;
    Hand.TurnRight += TurnRight;
}

private void OnDisable()
{
    Hand.TurnLeft -= TurnLeft;
    Hand.TurnRight -= TurnRight;
}

public void TurnLeft(Hand hand)
{
    Player.Rotate(-Angle);
}

public void TurnRight(Hand hand)
{
    Player.Rotate(Angle);
}

Компонент TurnPlayer навешиваем на каждую руку (Left Hand и Right Hand) и настраиваем.

Компонент TurnPlayer на примере левой руки.
Компонент TurnPlayer на примере левой руки.

Ну и в конце концов, создадим тег TeleportArea и повесим его на нашу плоскость. Этот тег позволит нам разделять в дальнейшем области, доступные для телепортации и недоступные для неё.

В итоге у нас должна получиться механика, благодаря которой мы можем более менее удобно перемещаться по нашей сцене. Протестируем её: запускаем Play Mode и отклоняем стик вперёд — луч телепорта начинает рисоваться красным, если телепортироваться в указанную область нельзя, и зеленым, если перемещение возможно. На отклонение стика влево и вправо мы будем поворачиваться в соответствующую сторону.

На этом благодарю за внимание. В следующей статье разберем простую механику взаимодействия с предметами: прикосновение к ним, их подбирание и дальнейшее использование.


Скоро в OTUS состоится бесплатное открытое занятие «AR сегодня: в развлечениях, в образовании, на работе». На нем узнаете, как AR технологии проникли во все сферы вашей жизни, и попробуете сами создать AR мини-игру. Регистрируйтесь по ссылке.

Комментарии (1)


  1. pokryshkin
    09.09.2022 17:52

    Ну хоть что-нибудь скажите про XR Interaction Toolkit, зачем без него лезть в дебри, если это статья для начинающих?