Продолжим серию статей про 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.
Перед началом работы с инпутом, необходимо повторить функционал из предыдущего урока: перемещение капсул за руками. В прошлом уроке мы просто повесили капсулы на 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 и настроим их.
Поворот игрока
Создадим класс 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) и настраиваем.
Ну и в конце концов, создадим тег TeleportArea и повесим его на нашу плоскость. Этот тег позволит нам разделять в дальнейшем области, доступные для телепортации и недоступные для неё.
В итоге у нас должна получиться механика, благодаря которой мы можем более менее удобно перемещаться по нашей сцене. Протестируем её: запускаем Play Mode и отклоняем стик вперёд — луч телепорта начинает рисоваться красным, если телепортироваться в указанную область нельзя, и зеленым, если перемещение возможно. На отклонение стика влево и вправо мы будем поворачиваться в соответствующую сторону.
На этом благодарю за внимание. В следующей статье разберем простую механику взаимодействия с предметами: прикосновение к ним, их подбирание и дальнейшее использование.
Скоро в OTUS состоится бесплатное открытое занятие «AR сегодня: в развлечениях, в образовании, на работе». На нем узнаете, как AR технологии проникли во все сферы вашей жизни, и попробуете сами создать AR мини-игру. Регистрируйтесь по ссылке.
pokryshkin
Ну хоть что-нибудь скажите про XR Interaction Toolkit, зачем без него лезть в дебри, если это статья для начинающих?