Unity XR Вводимые ресурсы
Этот раздел руководства пользователя Unity рассказывает обо всех поддерживаемых Unity устройствах ввода для приложений виртуальной и дополненной реальности, а также Windows Mixed Reality. На этой странице рассматриваются следующие темы:
- XR входные сопоставления
- Доступ к устройствам ввода
- Доступ к функциям ввода на устройстве ввода
- Доступ к вводу XR через устаревшую систему ввода
- Хаптики
Платформы XR имеют богатое разнообразие функций ввода, которыми вы можете воспользоваться при разработке взаимодействия с пользователями. Ваше приложение может использовать конкретные данные, которые относятся к положениям, вращениям, касаниям, кнопкам, джойстикам и датчикам пальцев. Однако доступ к этим функциям ввода может сильно различаться между платформами. Например, существуют небольшие различия между Vive и Oculus Rift, но настольная платформа с поддержкой VR и мобильная платформа, такая как Daydream, отличаются гораздо больше.
Unity предоставляет структуру C# с именем InputFeatureUsage, который определяет стандартный набор физических элементов управления устройствами (таких как кнопки и триггеры) для доступа к вводу пользователя на любой платформе. Они помогают идентифицировать типы ввода по названию. См. XR.Input.CommonUsages для определения каждого из InputFeatureUsage.
Каждый InputFeatureUsage соответствует общему действию или типу ввода. Например, Unity определяет InputFeatureUsage, называемый trigger, как одноосевой ввод, который управляется указательным пальцем, независимо от того, какую платформу XR вы используете. Вы можете использовать InputFeatureUsage, чтобы получить trigger состояние по его имени, так что вам не нужно настраивать ось (или кнопку на некоторых XR-платформах) для обычной системы ввода Unity.
XR входные сопоставления
В следующей таблице перечислены стандартные имена контроллеров InputFeatureUsage и их сопоставление с контроллерами популярных систем XR:
| InputFeatureUsage | Тип функции | Старый индекс входа (левый контроллер/правый контроллер) | WMR | Окулус | GearVR | Мечта | OpenVR (полный) | Живые | Окуляр через OpenVR | WMR через OpenVR |
|---|---|---|---|---|---|---|---|---|---|---|
| primary2DAxis | Ось 2D | [(1,2)/(4,5)] | Тачпад | Джойстик | Тачпад | Тачпад | Трекпад/Джойстик | Трекпад | Джойстик | Джойстик |
| trigger | Ось | [9/10] | Триггер | Триггер | Триггер | Триггер | Триггер | Триггер | Триггер | Триггер |
| grip | Ось | [11/12] | Грипп | Грипп | Бампер | Грипп | Грипп | Грипп | Грипп | |
| secondary2DAxis | Ось 2D | [(17,18)/(19,20)] | Джойстик | Тачпад | ||||||
| secondary2DAxisClick | Кнопка | [18/19] | Джойстик — Нажмите | |||||||
| primaryButton | Кнопка | [2/0] | [X/A] — пресса | Приложение | Начальная школа | Основной (сендвич-кнопка)(1) | Начальное образование (мужчины и женщины) | Меню | ||
| primaryTouch | Кнопка | [12/10] | [X/A] — касание | |||||||
| secondaryButton | Кнопка | [3/1] | [Д/В] — пресса | Альтернативный член | Заместитель (X/A) | |||||
| secondaryTouch | Кнопка | [13/11] | [Y/B] — касание | |||||||
| gripButton | Кнопка | [4/5] | Сцепление — пресс | Сцепление — пресс | Сцепление — пресс | Сцепление — пресс | Сцепление — пресс | Сцепление — пресс | ||
| triggerButton | Кнопка | [14/15] | Триггер — Нажмите | Триггер — Нажмите | Триггер — Нажмите | Триггер — Нажмите | Триггер — Нажмите | Триггер — Нажмите | Триггер — касание | Триггер — Нажмите |
| menuButton | Кнопка | [6/7] | Меню | Запуск (только левый контроллер) | ||||||
| primary2DAxisClick | Кнопка | [8/9] | Тачпад — Нажмите | Палка — пресс | Тачпад — Нажмите | Тачпад — Нажмите | Трекпад/Джойстик — Нажмите | Трекпад — Нажмите | Джойстик — пресс | Тачпад — Нажмите |
| primary2DAxisTouch | Кнопка | [16/17] | Тачпад — сенсорный | Палочка — касание | Тачпад — сенсорный | Тачпад — сенсорный | Трекпад/Джойстик — Сенсорный | Трекпад — сенсорный | Джойстик — сенсорный | Тачпад — сенсорный |
| batteryLevel | Ось | Уровень заряда батареи | ||||||||
| userPresence | Кнопка | Присутствие пользователя | Присутствие пользователя |
(1) Сандвич-кнопка относится к кнопке меню Vive. Эта кнопка сопоставлена с primaryButton, а не с menuButton, чтобы лучше обрабатывать кросс-платформенные приложения.
См. XR.Input.CommonUsages для определения каждого InputFeatureUsage.
Доступ к устройствам ввода
InputDevice представляет любое физическое устройство, например контроллер, мобильный телефон или гарнитуру. В нем может содержаться информация о отслеживании устройства, кнопках, джойстиках и других элементах управления вводами. Дополнительные сведения о InputDevice API см. в документации по InputDevice.
Используйте XR.InputDevices класс, чтобы обращаться к устройствам ввода, подключённым к системе XR в данный момент. Чтобы получить список всех подключённых устройств, используйте InputDevices.GetDevices:
var inputDevices = new List<UnityEngine.XR.InputDevice>();
UnityEngine.XR.InputDevices.GetDevices(inputDevices);
foreach (var device in inputDevices)
{
Debug.Log(string.Format("Device found with name '{0}' and role '{1}'", device.name, device.role.ToString()));
}
Устройство ввода остается действительным на всех кадрах до тех пор, пока система XR не отключит его. Используйте свойства InputDevice.IsValid, чтобы определить, является ли InputDevice все еще активным контроллером.
Доступ к устройствам ввода можно получить следующим образом:
- Характеристики
- Роль
- узел XR
Доступ к устройствам ввода по характеристикам
Характеристики устройства описывают, на что способно устройство или для чего оно используется (например, устанавливается ли оно на голову). InputDeviceCharacteristics — это ряд флагов, которые можно добавить в код для поиска устройств, соответствующих определенной спецификации. Вы можете фильтровать устройства по следующим характеристикам:
| Устройство | Характеристика |
|---|---|
| HeadMounted | Устройство прикрепляется к голове пользователя. У него есть отслеживание устройства и отслеживание глаза в центре. Этот флаг наиболее часто используется для идентификации головных дисплеев (HMDs). |
| Camera | Устройство имеет отслеживание камеры. |
| HeldInHand | Пользователь держит устройство в руке. |
| HandTracking | Устройство представляет собой физически отслеживаемую руку. Оно имеет отслеживание устройства и может содержать данные руки и кости. |
| EyeTracking | Устройство может выполнять eye tracking и имеет EyesData функцию. |
| TrackedDevice | Устройство может быть отслежено в 3D пространстве. У него есть отслеживание устройства. |
| Controller | Устройство имеет входные данные для кнопок и осей и может использоваться в качестве контроллера. |
| TrackingReference | Устройство представляет статический опорный объект отслеживания. Отслеживание у него есть, но его данные не должны меняться. |
| Left | Используйте эту характеристику в сочетании с характеристиками HeldInHand или HandTracking для идентификации устройства как связанного с левой рукой. |
| Right | Используйте эту характеристику в сочетании с характеристиками HeldInHand или HandTracking для идентификации устройства как связанного с правой рукой. |
| Simulated6DOF | Устройство сообщает данные 6DOF, но имеет только датчики 3DOF. Unity имитирует позиционные данные. |
Плагин поставщика XR сообщает об этих характеристиках. Вы можете просмотреть их с помощью InputDevice.Characteristics. Устройство может, и часто должно, иметь несколько характеристик, которые вы можете отфильтровать и получить доступ к которым с помощью битовых флагов.
InputDevices.GetDevicesWithCharacteristics обеспечивает поиск всех устройств с заданным набором характеристик. Например, вы можете искать Left, HeldInHand, Controller InputDevices, доступные в системе, с помощью следующего кода:
var leftHandedControllers = new List<UnityEngine.XR.InputDevice>();
var desiredCharacteristics = UnityEngine.XR.InputDeviceCharacteristics.HeldInHand | UnityEngine.XR.InputDeviceCharacteristics.Left | UnityEngine.XR.InputDeviceCharacteristics.Controller;
UnityEngine.XR.InputDevices.GetDevicesWithCharacteristics(desiredCharacteristics, leftHandedControllers);
foreach (var device in leftHandedControllers)
{
Debug.Log(string.Format("Device name '{0}' has characteristics '{1}'", device.name, device.characteristics.ToString()));
}
Устройства, найденные этой функцией, содержат по крайней мере указанные характеристики, но могут содержать дополнительные характеристики. Например, чтобы найти контроллер для левых рук, вы можете искать только InputDeviceCharacteristic.Left, а не InputDeviceCharacteristic.Controller.
Доступ к устройствам ввода по ролям
Роль устройства описывает общую функцию устройства ввода. Используйте перечисление InputDeviceRole для указания роли устройства. Определяются следующие роли:
| Роль | Описание |
|---|---|
| GameController | Контроллер игры в стиле консоли. |
| Generic | Устройство, представляющее основное устройство XR, например, головной дисплей или мобильное устройство. |
| HardwareTracker | Устройство слежения. |
| LeftHanded | Устройство, связанное с левой рукой пользователя. |
| RightHanded | Устройство, связанное с правой рукой пользователя. |
| TrackingReference | Устройство, которое отслеживает другие устройства, например камера отслеживания Oculus. |
Плагин поставщика XR сообщает об этих ролях, но разные поставщики могут по-разному организовывать свои роли устройств. Кроме того, пользователь может сменить руки, так что назначение роли может не соответствовать руке, в которой пользователь держит устройство ввода. Например, пользователь должен настроить контроллер Daydream как правшую или левую руку, но может выбрать, чтобы держать контроллер в противоположной руке.
GetDevicesWithRole предоставляет список любых устройств с конкретным InputDeviceRole. Например, вы можете использовать InputDeviceRole.GameController для получения всех подключенных устройств GameController:
var gameControllers = new List<UnityEngine.XR.InputDevice>();
UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.GameController, gameControllers);
foreach (var device in gameControllers)
{
Debug.Log(string.Format("Device name '{0}' has role '{1}'", device.name, device.role.ToString()));
}
Доступ к устройствам ввода узлом XR
Узлы XR представляют собой физические точки отсчета в системе XR (например, положение головы пользователя, его правая и левая руки, или отслеживание ссылок, таких как камера Oculus).
Перечисление XRNode определяет следующие узлы:
| XR узел | Описание |
|---|---|
| CenterEye | Точка, расположенная на полпути между зрачками глаз пользователя. |
| GameController | Игровой контроллер в стиле консоли. В приложении может быть несколько устройств игрового контроллера. |
| HardwareTracker | Устройство отслеживания оборудования, обычно прикрепленное к пользователю или физическому элементу. Может существовать несколько узлов отслеживания оборудования. |
| Head | Центральная точка головы пользователя, рассчитанная системой XR. |
| LeftEye | Левый глаз пользователя. |
| LeftHand | Левая рука пользователя. |
| RightEye | Правый глаз пользователя. |
| RightHand | Правая рука пользователя |
| TrackingReference | Отправная точка отслеживания, например камера Oculus. Может существовать несколько отправных узлов отслеживания. |
Используйте InputDevices.GetDevicesAtXRNode для получения списка устройств, связанных с конкретным XRNode. Следующий пример демонстрирует, как получить контроллер для левых:
var leftHandDevices = new List<UnityEngine.XR.InputDevice>();
UnityEngine.XR.InputDevices.GetDevicesAtXRNode(UnityEngine.XR.XRNode.LeftHand, leftHandDevices);
if(leftHandDevices.Count == 1)
{
UnityEngine.XR.InputDevice device = leftHandDevices[0];
Debug.Log(string.Format("Device name '{0}' with role '{1}'", device.name, device.role.ToString()));
}
else if(leftHandDevices.Count > 1)
{
Debug.Log("Found more than one left hand!");
}
Прослушивание подключений и отключений устройств
Устройства ввода последовательны от кадра к кадру, но могут подключаться или отключаться в любое время. Чтобы избежать повторяющейся проверки подключения устройства к платформе, используйте InputDevices.deviceConnected и InputDevices.deviceDisconnected для уведомления приложения о подключении или отключении устройства. Они также предоставляют ссылку на вновь подключенное устройство ввода.
Поскольку эти ссылки могут сохраняться в течение нескольких кадров, устройство может отключиться или иным образом стать недоступным. Чтобы проверить, доступны ли все еще входы устройства, используйте InputDevice.isValid. Скрипты, которые получают доступ к устройствам ввода, должны проверять это в начале каждого кадра, прежде чем пытаться использовать это устройство.
Доступ к функциям ввода на устройстве ввода
Вы можете считывать элемент ввода — например, состояние кнопки триггера — с конкретного InputDevice. Например, чтобы считать состояние правого триггера, выполните следующее:
- Используйте InputDeviceRole.RightHanded или XRNode.RightHand для получения экземпляра праворукого устройства.
- После того, как у вас есть правильное устройство, используйте метод InputDevice.TryGetFeatureValue для доступа к текущему состоянию.
TryGetFeatureValue() пытается получить доступ к текущему значению функции и возвращает:
- true, если успешно получено указанное значение объекта
- false, если текущее устройство не поддерживает указанную функцию, или устройство является недействительным (то есть контроллер больше не активен)
Чтобы получить определенную кнопку, сенсорный ввод или значение оси джойстика, используйте CommonUsages class. CommonUsages включает каждое InputFeatureUsage в 2000 году XR таблица сопоставления входов, а также функции отслеживания, такие как положение и вращение. В следующем примере кода используется CommonUsages.triggerButton для обнаружения того, нажимает ли пользователь в данный момент кнопку спускового крючка на определенном InputDevice экземпляр:
bool triggerValue;
if (device.TryGetFeatureValue(UnityEngine.XR.CommonUsages.triggerButton, out triggerValue) && triggerValue)
{
Debug.Log("Trigger button is pressed.");
}
Вы также можете использовать метод InputDevice.TryGetFeatureUsages для получения списка всех InputFeatureUsage, которые предоставляет устройство. Эта функция возвращает список элементов InputFeatureUsage, которые имеют свойства type и name, описывающие функцию. В следующем примере перечислены все логические функции, которые предоставляет заданное устройство ввода:
var inputFeatures = new List<UnityEngine.XR.InputFeatureUsage>();
if (device.TryGetFeatureUsages(inputFeatures))
{
foreach (var feature in inputFeatures)
{
if (feature.type == typeof(bool))
{
bool featureValue;
if (device.TryGetFeatureValue(feature.As<bool>(), out featureValue))
{
Debug.Log(string.Format("Bool feature {0}'s value is {1}", feature.name, featureValue.ToString()));
}
}
}
}
Пример для primaryButton
Различные конфигурации контроллеров обеспечивают доступ к различным функциям. Например, у вас может быть несколько контроллеров на одной системе, разные контроллеры на разных системах или разные кнопки на одних и тех же контроллерах с разными SDKs. Это разнообразие усложняет поддержку ввода из ряда XR систем. Unity InputFeatureUsage API помогает получить ввод, который не зависит от платформы.
В следующем примере доступ к InputFeatureUsage вызванный primaryButton, независимо от того, какой контроллер или устройство ввода его предоставляет. В примере есть класс, который просматривает доступные устройства в поисках primaryButton по мере их подключения. Класс отслеживает значение признака на всех подключённых устройствах и, если значение меняется, отправляет UnityEvent.
Чтобы использовать этот класс, добавьте его как компонент к любому GameObject в сцене. Например:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR;
[System.Serializable]
public class PrimaryButtonEvent : UnityEvent<bool> { }
public class PrimaryButtonWatcher : MonoBehaviour
{
public PrimaryButtonEvent primaryButtonPress;
private bool lastButtonState = false;
private List<InputDevice> devicesWithPrimaryButton;
private void Awake()
{
if (primaryButtonPress == null)
{
primaryButtonPress = new PrimaryButtonEvent();
}
devicesWithPrimaryButton = new List<InputDevice>();
}
void OnEnable()
{
List<InputDevice> allDevices = new List<InputDevice>();
InputDevices.GetDevices(allDevices);
foreach(InputDevice device in allDevices)
InputDevices_deviceConnected(device);
InputDevices.deviceConnected += InputDevices_deviceConnected;
InputDevices.deviceDisconnected += InputDevices_deviceDisconnected;
}
private void OnDisable()
{
InputDevices.deviceConnected -= InputDevices_deviceConnected;
InputDevices.deviceDisconnected -= InputDevices_deviceDisconnected;
devicesWithPrimaryButton.Clear();
}
private void InputDevices_deviceConnected(InputDevice device)
{
bool discardedValue;
if (device.TryGetFeatureValue(CommonUsages.primaryButton, out discardedValue))
{
devicesWithPrimaryButton.Add(device); // Add any devices that have a primary button.
}
}
private void InputDevices_deviceDisconnected(InputDevice device)
{
if (devicesWithPrimaryButton.Contains(device))
devicesWithPrimaryButton.Remove(device);
}
void Update()
{
bool tempState = false;
foreach (var device in devicesWithPrimaryButton)
{
bool primaryButtonState = false;
tempState = device.TryGetFeatureValue(CommonUsages.primaryButton, out primaryButtonState) // did get a value
&& primaryButtonState // the value we got
|| tempState; // cumulative result from other controllers
}
if (tempState != lastButtonState) // Button state changed since last frame
{
primaryButtonPress.Invoke(tempState);
lastButtonState = tempState;
}
}
}
Следующие PrimaryReactor класс использует PrimaryButtonWatcher чтобы определять нажатие основной кнопки и в ответ поворачивать родительский GameObject. Чтобы воспользоваться этим классом, добавьте его к видимому GameObject — например, к кубу — и перетащите PrimaryButtonWatcher ссылка на Наблюдатель свойства.
using System.Collections;
using UnityEngine;
public class PrimaryReactor : MonoBehaviour
{
public PrimaryButtonWatcher watcher;
public bool IsPressed = false; // used to display button state in the Unity Inspector window
public Vector3 rotationAngle = new Vector3(45, 45, 45);
public float rotationDuration = 0.25f; // seconds
private Quaternion offRotation;
private Quaternion onRotation;
private Coroutine rotator;
void Start()
{
watcher.primaryButtonPress.AddListener(onPrimaryButtonEvent);
offRotation = this.transform.rotation;
onRotation = Quaternion.Euler(rotationAngle) * offRotation;
}
public void onPrimaryButtonEvent(bool pressed)
{
IsPressed = pressed;
if (rotator != null)
StopCoroutine(rotator);
if (pressed)
rotator = StartCoroutine(AnimateRotation(this.transform.rotation, onRotation));
else
rotator = StartCoroutine(AnimateRotation(this.transform.rotation, offRotation));
}
private IEnumerator AnimateRotation(Quaternion fromRotation, Quaternion toRotation)
{
float t = 0;
while (t < rotationDuration)
{
transform.rotation = Quaternion.Lerp(fromRotation, toRotation, t / rotationDuration);
t += Time.deltaTime;
yield return null;
}
}
}
Доступ к данным отслеживания движения
InputDevices поддерживает устройства для отслеживания движения рук. Устройство для отслеживания движения рук всегда:
- имеет характеристику HandTracking
- содержит CommonUsages.HandData использование типа Hand
Данные для отслеживания движения руки состоят из объекта Hand и серии до 21 вводного элемента Bone. Каждый Bone имеет позицию и ориентацию, а также ссылки как на родительскую, так и на любую дочернюю кость в иерархии. Объект Hand может получить либо корневую кость, либо список костей для каждого пальца.
Когда Hand.TryGetRootBone получает корневую кость, он получает объект, представляющий кость, расположенную прямо над запястьем. Вы также можете получить список костей, представляющих каждый отдельный палец. Вызов Hand.TryGetFingerBones возвращает список костей, представляющих этот палец, от запястья до кончика.
Доступ к данным eye-tracking
Устройства ввода поддерживают устройства отслеживания глаз, а также устройства отслеживания рук. Отслеживание глаз состоит из положения левого и правого глаза, местоположения в пространстве 3D, куда пользователь смотрит, и количества мигания каждого глаза. Тип данных Eyes. Чтобы получить его с устройства, используйте CommonUsages.eyesData.
Ассоциация XRInputSubsystem и InputDevice
Unity предоставляет две системы ввода: устаревшую систему ввода и архитектуру плагина XR, представленную в 2019.2. В новой настройке каждый InputDevice ассоциируется с XRInputSubsystem. Эти объекты подсистемы контролируют глобальное поведение ввода, которое не ассоциируется с каким-либо конкретным устройством ввода (например, управление источником отслеживания или перенаправление отслеживаемых устройств).
Каждый InputDevice содержит ссылку на связанную с ним подсистему. Эта ссылка является нулевым, если устройство происходит от интегрированной платформы. Вы также можете получить все активные объекты XRInputSubsystem с помощью SubsystemManager.GetInstances<XRInputSubsystem>, и каждый XRInputSubsystem может получить свои устройства с помощью XRInputSubsystem.TryGetInputDevices.
Вы можете использовать подсистему ввода для перецентровки устройств с UnityEngine.XR.XRInputSubsystem. Recenter устанавливает текущее положение HMD в качестве нового источника для всех устройств. Он возвращает false для устройств, которые не могут быть перецентрованы, или если платформа не поддерживает перецентровку.
Для получения границы отслеживания используйте TryGetBoundaryPoints. Она состоит из ряда точек 3D, упорядоченных по часовой стрелке, где значение y находится на уровне пола, и они обозначают задаваемую пользователем «безопасную зону» для размещения контента и взаимодействий. Вы можете прослушивать изменения в этой границе с помощью XRInputSubsystem.boundaryChanged.
XRInputSubsystem также отвечает за режим происхождения отслеживания, который предоставляет контекст для того, где находится происхождение мира отслеживания. Unity поддерживает следующие режимы происхождения отслеживания:
- Device: Место происхождения находится в первом известном месте основного устройства отображения; часто это HMD или телефон.
- Floor: Место происхождения находится в известном месте на полу.
- Ссылка на отслеживание: Место происхождения находится на InputDevice с набором характеристик TrackingReference.
- Unknown: Тип источника отслеживания неизвестен. Это может быть из-за сбоя системы или отсутствия поддержки режима источника отслеживания.
Существует три APIs, которые можно использовать для управления режимом происхождения отслеживания:
- XRInputSubsystem.TrySetTrackingOriginMode устанавливает режим происхождения отслеживания
- XRInputSubystem.GetTrackingOriginMode получает режим происхождения отслеживания
- XRInputSubsystem.GetSupportedTrackingOriginModes получает все режимы источника отслеживания, поддерживаемые SDK
XR ввод через устаревшую систему ввода
Вы все еще можете использовать устаревшую систему ввода, состоящую из Input и XR.InputTracking, для извлечения XR Для этого используйте соответствующие устаревшие индексы ввода из раздела «Индексы ввода». XR таблица входных сопоставлений на этой странице. Вводимые ресурсы раздела бюджета Настройки Игрока (меню: Изменить > Project Настройки > Вводимые ресурсы), создать отображение оси, чтобы добавить соответствующее отображение от имени входа до индекса оси для функции устройства платформы. Чтобы получить значения кнопки или оси, используйте Input.GetAxis или Input.GetButton и передаёт имя отображаемой оси или кнопки.
Дополнительные сведения об использовании осей кнопки и джойстика см. в документации по InputManager.
Хаптики
Вы можете отправить тактильные события на InputDevice. Тактильные события принимают форму импульса, с амплитудой и продолжительностью.
Не все платформы поддерживают все типы тактильных ощущений, но вы можете запросить устройства для тактильных возможностей. В следующем примере получается устройство ввода для правой руки, проверяется, может ли устройство использовать тактильные ощущения, и затем воспроизводится импульс, если он может:
List<UnityEngine.XR.InputDevice> devices = new List<UnityEngine.XR.InputDevice>();
UnityEngine.XR.InputDevices.GetDevicesWithRole(UnityEngine.XR.InputDeviceRole.RightHanded, devices);
foreach (var device in devices)
{
UnityEngine.XR.HapticCapabilities capabilities;
if (device.TryGetHapticCapabilities(out capabilities))
{
if (capabilities.supportsImpulse)
{
uint channel = 0;
float amplitude = 0.5f;
float duration = 1.0f;
device.SendHapticImpulse(channel, amplitude, duration);
}
}
}