This page has been translated automatically.
Видеоуроки
Интерфейс
Основы
Продвинутый уровень
Подсказки и советы
Основы
Программирование на C#
Рендеринг
Профессиональный уровень (SIM)
Принципы работы
Свойства (properties)
Компонентная Система
Рендер
Физика
Редактор UnigineEditor
Обзор интерфейса
Работа с ассетами
Контроль версий
Настройки и предпочтения
Работа с проектами
Настройка параметров ноды
Setting Up Materials
Настройка свойств
Освещение
Sandworm
Использование инструментов редактора для конкретных задач
Расширение функционала редактора
Встроенные объекты
Ноды (Nodes)
Объекты (Objects)
Эффекты
Декали
Источники света
Geodetics
World-ноды
Звуковые объекты
Объекты поиска пути
Player-ноды
Программирование
Основы
Настройка среды разработки
Примеры использования
C++
C#
UnigineScript
UUSL (Unified UNIGINE Shader Language)
Плагины
Форматы файлов
Материалы и шейдеры
Rebuilding the Engine Tools
Интерфейс пользователя (GUI)
Двойная точность координат
API
Animations-Related Classes
Containers
Common Functionality
Controls-Related Classes
Engine-Related Classes
Filesystem Functionality
GUI-Related Classes
Math Functionality
Node-Related Classes
Objects-Related Classes
Networking Functionality
Pathfinding-Related Classes
Physics-Related Classes
Plugins-Related Classes
IG Plugin
CIGIConnector Plugin
Rendering-Related Classes
VR-Related Classes
Работа с контентом
Оптимизация контента
Материалы
Визуальный редактор материалов
Material Nodes Library
Miscellaneous
Input
Math
Matrix
Textures
Art Samples
Учебные материалы

Начало работы с VR

Примечание
The article reviews the creation of C# VR projects only. You can switch to the C++ version in the upper right corner of the page. В статье рассматривается создание VR проекта на C#. Переключиться на версию для C++ можно в правом верхнем углу страницы.

This article is for anyone who wants to start developing Virtual Reality projects in UNIGINE and is highly recommended for all new users. We're going to use the VR Template for C# to easily get started on our own project for VR. We will also look into the VR Sample, which can be used as the basis for a VR application, and consider some simple examples of extending the basic functionality of this sample. Эта статья предназначена для всех, кто хочет начать разработку проектов виртуальной реальности в UNIGINE, и настоятельно рекомендуется всем новым пользователям. Мы собираемся использовать готовый шаблон для разработки на C# под VR, позволяющий сразу приступить к созданию собственных проектов. Кроме того, мы рассмотрим пример проекта VR Sample, который можно взять за основу для вашего собственного VR-приложения, а также разберемся, как можно расширить его функционал.

So, let's get started! Итак, приступим!

1. Making a Template Project
1. Создание приложения на основе шаблона.
#

UNIGINE предлагает готовый шаблон для разработки на C# под VR. Он поддерживает Oculus Rift, HTC Vive / Vive Pro (OpenVR) "из коробки". Вместе с шаблоном доступен и пример проекта (VR Sample). Здесь вы найдете набор 3D-моделей популярных контроллеров VR, а также реализацию основных механик, таких как захват и толкание объектов, нажатие кнопок, открытие / закрытие ящиков и многое другое.

Примечание
Мир в этом примере имеет свои настройки, оптимизированные для наилучшей производительности в VR.

Both the template and the sample project are created using the Component System — components attached to objects determine their functionality.И шаблон, и пример проекта, созданы с использованием Компонентной системы, поэтому функциональность каждого объекта определяется набором назначенных ему компонентов.

Примечание
A brief overview of the template structure and a description of its main components are available through the link.Краткий обзор структуры шаблона и описание его базовых компонентов можно найти по ссылке.

You can extend object's functionality simply by assigning it more components. For example, the laser pointer object in the VR Sample has the following components attached:Вы можете расширить функциональность того или иного объекта, просто добавив к нему дополнительные компоненты. Например, к объекту лазерной указки в VR Sample прикреплены следующие компоненты:

You can also implement your own components to extend functionality.Кроме того, можно написать собственные компоненты, чтобы расширить существующий функционал.

The main features of the VR Sample:Базовый функционал примера VR Sample включает следующее:

  • Поддержка VR (Oculus Rift и HTC Vive/OpenVR)
  • A set of physical objects that can be manipulated via controllers (physical objects, buttons, drawers)Набор физических объектов, которыми можно управлять с помощью контроллеров (физические объекты, кнопки, выдвижной ящик)
  • An interactive laser pointer objectИнтерактивный объект "лазерная указка"
  • Teleportation around the sceneТелепортация по сцене
  • GUIПользовательский интерфейс
  • Interaction with GUI objects via controllersВзаимодействие с объектами графического интерфейса через контроллеры

Let's create a project based on the C# VR Template.Для начала, давайте создадим проект на основе шаблона C# VR Template.

  1. Open SDK Browser, go to the My Projects tab, and click Create New.Откройте SDK Browser, перейдите на вкладку My Projects и щелкните Create New.
  2. In the Create New Project window, specify the project name VRProject, select VR in the Template menu, then C#(.NET) in the API+IDE menu, and select Float in the Precision menu. Then click Create New Project. В открывшемся окне Create New Project задайте имя вашего проекта VRProject, в меню Template выберите шаблон приложения VR, а в меню Precision выберите Float. Затем нажмите Create New Project.
  3. When the project is successfully created, open it in UnigineEditor.После успешного создания проекта откройте его в Редакторе.
  4. In the Asset Browser, open the vr_sample.world file located in the data/vr_template/vr_sample/ folder. We will work with this world.Затем в Asset Browser откройте мир vr_sample.world, который лежит в папке data/vr_template/vr_sample/. С этим миром и будем работать.

2. Setting Up a Device and Configuring Project
2. Выбор устройства.
#

Предположим, вы успешно установили выбранный головной дисплей (HMD) (пожалуйста, посетите Oculus Rift Setup или HTC Vive Setup , если вы этого не сделали). Если у вас возникли проблемы с началом работы HTC Vive, может оказаться полезным это руководство по устранению неполадок .

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

HTC Vive Oculus Rift

Если вы запускаете приложение через UNIGINE SDK Browser, установите для параметра Stereo 3D значение HTC Vive на вкладке Global Options и нажмите Apply:

Для запуска приложения укажите соответствующий VR-режим:

Если вы запускаете приложение через UNIGINE SDK Browser, установите для параметра Stereo 3D значение Oculus Rift на вкладке Global Options и нажмите Apply:

Для запуска приложения укажите соответствующий VR-режим:

Примечание
Вы также можете указать плагин для вашего приложения через Customize Run Options.

Run the application and learn about basic mechanics, grab and throw objects, try to teleport and interact with objects in the scene.Запустите приложение и познакомьтесь с механиками в действии, попробуйте перемещение, телепортацию и взаимодействие со всеми интерактивным объектами в сцене.

3. Extending Functionality
3. Расширение имеющегося функционала.
#

So, as a part of our first VR project, let's learn how to use the VR Sample functionality and how it can be extended.Итак, в рамках нашего первого VR-проекта давайте разберемся на простых примерах, как пользоваться имеющимся в VR Sample функционалом и как его можно расширить.

3.1. Attaching Object to HMD
3.1. Привязка объекта к положению шлема.
#

Бывают ситуации, когда в приложении необходимо привязать положение того или иного объекта к шлему. Одним из примеров может служить меню (в сэмпле это нода MenuAttachedToHead) или, например, это могут быть очки с цветными стеклами, а может и дымящаяся сигара. Для того, чтобы привязать к положению шлема любую другую ноду, достаточно назначить ей компонент VRAttachToHead и указать нужные настройки (дистанцию, направление "вперед", фиксация положения и обновление положения при включении компонента).

3.3. Restricting Teleportations
3.2. Ограничение области телепортации.
#

In VR projects, sometimes we use teleporting to move around, especially in large scenes. By default, it is possible to teleport to any point of the scene. But sometimes, it might be necessary to restrict teleportation to certain areas (for example, to the roof). The VR Template provides the TeleportationMovement component (assigned to the vr_layervr_movement_managerteleportation_movement node) that implements teleportation logic and allows specifying the maximum teleportation distance, the appearance of the ray and target point, and so on.Телепортация – часто используемый способ перемещения в VR-проектах (особенно если размеры сцены больше комнаты). Иногда бывает нужно ограничить игрока, задать области, куда телепортироваться нельзя (например, на крышу дома). Сделать это с использованием шаблона очень просто. Механизм телепортации реализован в компоненте TeleportationMovement (назначен на ноду vr_layervr_movement_managerteleportation_movement). В параметрах компоненты можно задавать максимальную дальность телепортации, внешний вид луча и целевой точки и т.д.

You can teleport to an object if its Physics Intersection mask matches the Teleportation mask in the component settings. By default, the 2nd bit of the Teleportation mask is used. So, you can teleport to objects, for which the Physics Intersection option is enabled and the 2nd bit of the Physics Intersection mask is set.Чтобы на объект можно было телепортироваться, его маска Physics Intersection должна совпадать с Teleportation Mask, заданной в настройках компонента. По умолчанию используется второй бит, поэтому телепортироваться можно на все объекты, у которых включена опция Physics Intersection и выставлен второй бит маски.

To restrict teleportation to a certain area, do the following:Чтобы задать область, недоступную для телепортации, сделайте следующее:

  1. Create a new Static Mesh object by using the default plane (core/meshes/plane.mesh) or an arbitrary mesh that will define the restricted area.Создайте объект Static Mesh с использованием стандартной плоскости (core/meshes/plane.mesh) или произвольного меша, который будет задавать закрытую для пользователя область.
  2. Place the Static Mesh in the scene just above the floor.Поместите Static Mesh в нужное место на уровне чуть выше уровня пола.
  3. In the Parameters window, enable the Physics Intersection option for the object and check that the 2nd bit of the Physics Intersection Mask is disabled.

    В окне параметров включите у объекта опцию Physics Intersection и проверьте, чтобы второй бит маски был сброшен.

  4. Save and run the application. Now you cannot teleport to the area defined by the rectangular plane (the ray and the target point are red).

    Сохраните и запустите проект – в область прямоугольной плоскости теперь телепортироваться нельзя (луч и точка красные).

  5. To hide the Static Mesh, disable all bits of its Viewport and Shadow masks.

    Если нужно скрыть отображение Static Mesh, задающего область, достаточно сбросить его Viewport и Shadow маски.

  6. Save changes and run the application: the rectangular plane is invisible now.

    Теперь все работает и наша вспомогательная плоскость не видна.

3.4. Changing Controller Grip Button to Trigger
3.3. Изменение кнопки захвата для контроллера Vive на Trigger.
#

При разработке приложений часто бывает нужно переназначить действия или поменять местами те или иные элементы управления. Разберем это на примере Vive -контроллера. Переназначим использование (Use) c кнопки Grip на Trigger, а захват объекта (Grab) – наоборот, на Grip.

Пользовательский ввод для Vive -контроллера определяется в компоненте ViveControllerInput. В сцене нашего сэмпла этот компонент назначен на ноды left_preset_0 и right_preset_0 (vr_playervr_inputvive_input).

To remap actions, do the following:Чтобы переназначить действия, сделайте следующее:

  1. Select the left_preset_0 node that contains settings for the left controller (the Bind → Side parameter value is set to LEFT).Выберите ноду left_preset_0 с настройками для левого контроллера (в параметре Bind → Side здесь выбрана сторона LEFT).
  2. Change the Grab Button → Button value to LEFT_AXIS1, and the Use Button → Button value to LEFT_GRIP:

    Поменяйте Grab Button → Button на LEFT_AXIS, а Use Button → Button на LEFT_GRIP:

  3. Select the right_preset_0 node that contains settings for the right controller.Выберите ноду right_preset_0 с настройками для правого контроллера.
  4. Change the Grab Button → Button value to RIGHT_AXIS1, and the Use Button → Button value to RIGHT_GRIP.Поменяйте Grab Button → Button соответственно на RIGHT_AXIS, а Use Button → Button на RIGHT_GRIP.
  5. Save changes (Ctrl+S) and press the Play button to run the application.Сохраните мир (Ctrl+S) и нажмите кнопку Play.

Now you can grab objects by the trigger, and use them by the Grip side button.Теперь захват объекта осуществляется курком, а использование – боковой кнопкой Grip.

3.5. Adding an Interactive Area
3.4. Интерактивная зона.
#

In games and VR applications, some actions are performed when you enter a special area in the scene (for example, music starts, sound or visual effects appear). Let's implement this in our project: when you enter or teleport to a certain area, it rains inside it.В играх и VR-приложениях часто бывает нужно при вхождении пользователя в определенную область сцены выполнять определенные действия (например, включать музыку, звуковые или визуальные эффекты). В нашем проекте давайте сделаем так, чтобы при входе или телепортации пользователя в определенную область в ее пределах шел дождь.

In UNIGINE, the World Trigger node is used to track when any node (collider or not) gets inside or outside of it (for objects with physical bodies, the Physical Trigger node is used).В UNIGINE для отслеживания вхождения любой ноды (не важно, с физическим телом или без) в определенный объем и покидания этого объема используется World Trigger (для объектов с физическим телом используется Physical Trigger).

  1. First of all, create a trigger to define the rain area. On the Menu bar, choose Create → Logic → World Trigger and place the trigger near the Material Ball, for example. Set the trigger size to (2, 2, 2).

    Первым делом, создайте триггер для определения области, где будет идти дождь. Выберите в меню Create → Logic → World Trigger и расположите триггер, например, рядом с Material Ball. Установите размеры триггера (2, 2, 2).

  2. Create a new TriggerZone component that will turn the rain node on and off (in our case, it is a particle system simulating rain), depending on the trigger state. Add the following code to the component and then assign it to the World Trigger node:

    TriggerZone.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- an identifier is generated automatically for the component
    public class TriggerZone : Component
    {
    	private WorldTrigger trigger = null;
    	public Node ControlledNode = null;
    	private void Init()
    	{
    		// check if the component is assigned to WorldTrigger
    		if (node.Type != Node.TYPE.WORLD_TRIGGER)
    		{
    			Log.Error($"{nameof(TriggerZone)} error: this component can only be assigned to a WorldTrigger.\n");
    			Enabled = false;
    			return;
    		}
    		// check if ControlledNode is specified
    		if (ControlledNode == null)
    		{
    			Log.Error($"{nameof(TriggerZone)} error: 'ControlledNode' is not set.\n");
    			Enabled = false;
    			return;
    		}
    		// add event handlers to be fired when a node gets inside and outside the trigger
    		trigger = node as WorldTrigger;
    		trigger.EventEnter.Connect(trigger_enter);
    		trigger.EventLeave.Connect(trigger_leave);
    	}
    	
    	private void Update()
    	{
    		// write here code to be called before updating each render frame
    		
    	}
    
    	// handler function that enables ControlledNode when vr_player enters the trigger
    	private void trigger_enter(Node node)
    	{
    		ControlledNode.Enabled = true;
    	}
    	// handler function that disables ControlledNode when vr_player leaves the trigger
    	private void trigger_leave(Node node)
    	{
    		if (node.Name != "vr_player")
    			return;
    		ControlledNode.Enabled = false;
    	}
    }
    Создайте новый компонент TriggerZone, который в зависимости от состояния триггера будет включать или выключать заданную ноду (в нашем случае это будет система частиц имитирующая дождь). Добавьте в компонент следующий код и затем назначьте его ноде World Trigger:

    TriggerZone.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // идентификатор генерируется автоматически для компонента
    public class TriggerZone : Component
    {
    	private WorldTrigger trigger = null;
    	public Node ControlledNode = null;
    	private void Init()
    	{
    		// проверяем что компонент назначен на ноду WorldTrigger
    		if (node.Type != Node.TYPE.WORLD_TRIGGER)
    		{
    			Log.Error($"{nameof(TriggerZone)} error: this component can only be assigned to a WorldTrigger.\n");
    			Enabled = false;
    			return;
    		}
    		// проверяем указана ли управляемая нода 'ControlledNode'
    		if (ControlledNode == null)
    		{
    			Log.Error($"{nameof(TriggerZone)} error: 'ControlledNode' is not set.\n");
    			Enabled = false;
    			return;
    		}
    		// устанавливаем функции, которые будут вызываться при входе произвольной ноды в объем триггера и покидании его
    		trigger = node as WorldTrigger;
    		trigger.AddEnterCallback(trigger_enter);
    		trigger.AddLeaveCallback(trigger_leave);
    	}
    	
    	private void Update()
    	{
    		// write here code to be called before updating each render frame
    		
    	}
    
    	// функция, включающая ноду ControlledNode при вхождении игрока (нода 'vr_player') в триггер
    	private void trigger_enter(Node node)
    	{
    		ControlledNode.Enabled = true;
    	}
    	// функция, выключающая ноду ControlledNode при покидании триггера игроком (нода 'vr_player')
    	private void trigger_leave(Node node)
    	{
    		if (node.Name != "vr_player")
    			return;
    		ControlledNode.Enabled = false;
    	}
    }
  3. Now let's add the rain effect. Drag the rain_effect.node asset (available in the vr/particles folder of the UNIGINE Starter Course Project add-on) to the scene, add it as the child to the trigger node and turn it off (the component will turn it on later).

    Теперь добавьте эффект дождя. Найдите ассет rain_effect.node (хранится в папке vr/particles аддона UNIGINE Starter Course Project), добавьте его в проект, а затем перетащите его в сцену, добавьте в качестве дочерней ноды к триггеру и временно отключите (ее будет включать наш компонент).

  4. Drag the rain_effect node from the World Hierarchy window to the Controlled Node field of the TriggerZone (in the Parameters window).

    Затем перетащите ноду rain_effect из World Hierarchy в поле Controlled Node компонента TriggerZone.

  5. Save changes (Ctrl+S), click Play to run the application, and try to enter the area defined by the trigger — it is raining inside.

    Сохраните мир (Ctrl+S), запустите проект, нажав кнопку Play, и попробуйте войти в заданную область – в ней будет локальный дождь.

3.6. Adding a New Interaction
3.5. Смена материала лазерной указкой.
#

Another way to extend functionality is to add a new action for the object.Еще один вариант расширения функционала, который может пригодиться – это добавление нового действия.

We have a laser pointer that we can grab, throw, and use (turn on a laser beam). Now, let's add an alternative use action - change the pointed object's material. This action will be executed when we press and hold the Grip side button on one controller while using the laser pointer gripped by the opposite controller.Предположим, мы хотим расширить функциональность лазерной указки в нашем примере, которую на данный момент мы можем захватывать, удерживать, бросать и использовать (включать луч), добавив дополнительное действие – циклическая смена материалов по заданному списку на объекте, на который указывает луч. Это действие будет выполняться, если при использовании указки, захваченной одним контроллером, мы зажмем боковую кнопку использования (Grip) на противоположном контроллере.

The VRLaserPointer.cs component implements the base functionality of the laser pointer.Базовый функционал лазерной указки реализован в компоненте VRLaserPointer.cs.

The base component for all interactive objects is VRBaseInteractable (vr_template/components/base/VRInteractable.cs). It defines all available states of the interactive objects (NOT_INTERACT, HOVERED, GRABBED, USED) and methods executed on certain actions performed on the objects (OnHoverBegin, OnHoverEnd, OnGrabBegin, OnGrabEnd, OnUseBegin, OnUseEnd).Основным компонентом, определяющим базовый функционал всех интерактивных объектов, является VRBaseInteractable(vr_template/components/base/VRInteractable.cs). Именно здесь определяются возможные состояния интерактивных объектов (NOT_INTERACT, HOVERED, GRABBED, USED) и методы, вызываемые при определенных действиях с объектом (OnHoverBegin, OnHoverEnd, OnGrabBegin, OnGrabEnd, OnUseBegin, OnUseEnd).

Components inherited from VRBaseInteractable must implement overrides of these methods (for example, the grab logic for the object must be implemented in the OnGrabBegin method of the inherited component).В унаследованных компонентах интерактивных объектов логика, которая должна выполняться при том или ином действии, реализуется в перегрузке соответствующего метода (например, при захвате объекта вызывается OnGrabBegin – в нем и пишется нужная логика).

So, we need to add a new action and map it to a new state (the Grip side button is pressed on one controller while the laser pointer gripped (Grip) by the opposite controller is used):Нам нужно добавить новое действие, которое будет выполняться при нажатии боковой кнопки Grip на одном контроллере во время использования лазерной указки, захваченной противоположным контроллером (зажата боковая кнопка Grip).

  1. Add a new OnAltUse action (alternative use) and a new ALT_USED state to the VRBaseInteractable base component:

    VRBaseInteractable.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- an identifier is generated automatically for the component
    public class VRBaseInteractable : Component
    {
    	static public event Action onInit;
    
    	public enum INTERACTABLE_STATE
    	{
    		NOT_INTERACT,
    		HOVERED,
    		GRABBED,
    		USED,
    		ALT_USED   // add a new state
    	}
    
    	public INTERACTABLE_STATE CurrentState { get; set; }
    
    	protected void Init()
    	{
    		onInit?.Invoke(this);
    	}
    
    	public virtual void OnHoverBegin(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnHoverEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnGrabBegin(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnGrabEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnUseBegin(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnUseEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    	// add methods for a new "alternative use" action
    	public virtual void OnAltUse(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnAltUseEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    }
    Добавьте новое действие OnAltUse (альтернативное использование) и новое состояние ALT_USED в базовый компонент VRBaseInteractable:

    VRBaseInteractable.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // идентификатор генерируется автоматически для компонента
    public class VRBaseInteractable : Component
    {
    	static public event Action<VRBaseInteractable> onInit;
    
    	public enum INTERACTABLE_STATE
    	{
    		NOT_INTERACT,
    		HOVERED,
    		GRABBED,
    		USED,
    		ALT_USED   // Добавляем новое состояние
    	}
    
    	public INTERACTABLE_STATE CurrentState { get; set; }
    
    	protected void Init()
    	{
    		onInit?.Invoke(this);
    	}
    
    	public virtual void OnHoverBegin(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnHoverEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnGrabBegin(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnGrabEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnUseBegin(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnUseEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    	// Добавляем новое действие и его прекращение
    	public virtual void OnAltUse(VRBaseInteraction interaction, VRBaseController controller) { }
    	public virtual void OnAltUseEnd(VRBaseInteraction interaction, VRBaseController controller) { }
    }
  2. Add overrides of the OnAltUse() and OnAltUseEnd() methods to the VRLaserPointer component (as we are going to add a new action for the laser pointer). In the Update() method, implement the logic of the AltUse action that is executed when the corresponding flags are set.

    VRLaserPointer.cs

    Исходный код (C#)
    #region Math Variables
    #if UNIGINE_DOUBLE
    	using Scalar = System.Double;
    	using Vec2 = Unigine.dvec2;
    	using Vec3 = Unigine.dvec3;
    	using Vec4 = Unigine.dvec4;
    	using Mat4 = Unigine.dmat4;
    #else
    using Scalar = System.Single;
    using Vec2 = Unigine.vec2;
    using Vec3 = Unigine.vec3;
    using Vec4 = Unigine.vec4;
    using Mat4 = Unigine.mat4;
    using WorldBoundBox = Unigine.BoundBox;
    using WorldBoundSphere = Unigine.BoundSphere;
    using WorldBoundFrustum = Unigine.BoundFrustum;
    #endif
    #endregion
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- an identifier is generated automatically for the component
    public class VRLaserPointer : VRBaseInteractable
    {
    	[ShowInEditor]
    	[Parameter(Title = "Laser", Group = "VR Laser Pointer")]
    	private Node laser = null;
    	
    	[ShowInEditor]
    	[Parameter(Title = "Laser Ray", Group = "VR Laser Pointer")]
    	private Node laserRay = null;
    
    	[ShowInEditor]
    	[Parameter(Title = "Laser Hit", Group = "VR Laser Pointer")]
    	private Node laserHit = null;
    
    	[ShowInEditor]
    	[Parameter(Title = "Object Text", Group = "VR Laser Pointer")]
    	private ObjectText objText = null;
    
    
    	private Mat4 laserRayMat;
    	private WorldIntersection intersection = new WorldIntersection();
    	private float rayOffset = 0.05f;
    
    	private bool grabbed = false;
    
    	private bool altuse = false;
    	public  List<Material> materials = null;
    	private int current_material = 0;
    	
    	protected override void OnReady()
    	{
    	laserRayMat = new Mat4(laserRay.Transform);
    	laser.Enabled = false;
    	// if the list of textures is empty/not defined, report an error
    	if(materials == null || materials.Count < 0)
    	{
    		Log.Error($"{nameof(VRLaserPointer)} error: materials list is empty.\n");
    		Enabled = false;
    		return;
    	}
    	}
    
    	private void Update()
    	{
    		if(laser.Enabled && grabbed)
    		{
    			laserRay.Transform = laserRayMat;
    
    			vec3 dir = laserRay.GetWorldDirection(MathLib.AXIS.Y);
    			Vec3 p0 = laserRay.WorldPosition + dir * rayOffset;
    			Vec3 p1 = p0 + dir * 1000;
    			Unigine.Object hitObj = World.GetIntersection(p0, p1, 1, intersection);
    			if(hitObj != null)
    			{
    				laserRay.Scale = new vec3(laserRay.Scale.x, 
    									MathLib.Length(intersection.Point - p0) + rayOffset, 
    									laserRay.Scale.z);
    				laserHit.WorldPosition = intersection.Point;
    				laserHit.Enabled = true;
    			}
    			else
    			{
    				laserHit.Enabled = false;
    			}
    
    			if (hitObj != null)
    			{
    				objText.Enabled = true;
    				objText.Text = hitObj.Name;
    				float radius = objText.BoundSphere.Radius;
    				vec3 shift = vec3.UP * radius;
    				objText.WorldTransform = MathLib.SetTo(laserHit.WorldPosition + shift, 
    										VRPlayer.LastPlayer.HeadController.WorldPosition, 
    										vec3.UP, 
    										MathLib.AXIS.Z);
    
    				// implement logic of the "alternative use" action (cyclic change of materials on the object according to the list)
    				if(altuse)
    				{
    					current_material++;
    					if (current_material >= materials.Capacity)
    						current_material = 0;
    
    						hitObj.SetMaterial(materials[current_material], 0);
    					Log.Message("ALTUSE HIT\n");
    					altuse = false;
    				}
    			}
    			else
    			objText.Enabled = false;
    		}
    	}
    
    	public override void OnGrabBegin(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		grabbed = true;
    	}
    
    	public override void OnGrabEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		grabbed = false;
    		laser.Enabled = false;
    		objText.Enabled = false;
    	}
    
    	public override void OnUseBegin(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		if(grabbed)
    			laser.Enabled = true;
    	}
    
    	public override void OnUseEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		laser.Enabled = false;
    		objText.Enabled = false;
    	}
    
    	// override the method that is called when the "alternative use" action is performed on the object
    	public override void OnAltUse(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		altuse = true;
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.ALT_USED;
    	}
    
    	// override the method that is called when the "alternative use" action is done
    	public override void OnAltUseEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		altuse = false;
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.NOT_INTERACT;
    	}
    }
    Затем добавьте перегрузку методов OnAltUse() и OnAltUseEnd() в компонент VRLaserPointer (поскольку именно для указки мы хотим добавить новое действие). А также добавьте код в метод Update(), выполняющий само действие AltUse, когда установлены соответствующие флаги.

    VRLaserPointer.cs

    Исходный код (C#)
    #region Math Variables
    #if UNIGINE_DOUBLE
    	using Scalar = System.Double;
    	using Vec2 = Unigine.dvec2;
    	using Vec3 = Unigine.dvec3;
    	using Vec4 = Unigine.dvec4;
    	using Mat4 = Unigine.dmat4;
    #else
    using Scalar = System.Single;
    using Vec2 = Unigine.vec2;
    using Vec3 = Unigine.vec3;
    using Vec4 = Unigine.vec4;
    using Mat4 = Unigine.mat4;
    using WorldBoundBox = Unigine.BoundBox;
    using WorldBoundSphere = Unigine.BoundSphere;
    using WorldBoundFrustum = Unigine.BoundFrustum;
    #endif
    #endregion
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- идентификатор генерируется автоматически для компонента
    public class VRLaserPointer : VRBaseInteractable
    {
    	[ShowInEditor]
    	[Parameter(Title = "Laser", Group = "VR Laser Pointer")]
    	private Node laser = null;
    	
    	[ShowInEditor]
    	[Parameter(Title = "Laser Ray", Group = "VR Laser Pointer")]
    	private Node laserRay = null;
    
    	[ShowInEditor]
    	[Parameter(Title = "Laser Hit", Group = "VR Laser Pointer")]
    	private Node laserHit = null;
    
    	[ShowInEditor]
    	[Parameter(Title = "Object Text", Group = "VR Laser Pointer")]
    	private ObjectText objText = null;
    
    
    	private Mat4 laserRayMat;
    	private WorldIntersection intersection = new WorldIntersection();
    	private float rayOffset = 0.05f;
    
    	private bool grabbed = false;
    
    	private bool altuse = false;
    	public  List<Material> materials = null;
    	private int current_material = 0;
    	
    	protected override void OnReady()
    	{
    		laserRayMat = new Mat4(laserRay.Transform);
    		laser.Enabled = false;
    		// если список текстур не задан или пуст, сообщаем об ошибке
    		if(materials == null || materials.Count < 0)
    		{
    			Log.Error($"{nameof(VRLaserPointer)} error: materials list is empty.\n");
    			Enabled = false;
    			return;
    		}
    	}
    
    	private void Update()
    	{
    		if(laser.Enabled && grabbed)
    		{
    			laserRay.Transform = laserRayMat;
    
    			vec3 dir = laserRay.GetWorldDirection(MathLib.AXIS.Y);
    			Vec3 p0 = laserRay.WorldPosition + dir * rayOffset;
    			Vec3 p1 = p0 + dir * 1000;
    			Unigine.Object hitObj = World.GetIntersection(p0, p1, 1, intersection);
    			if(hitObj != null)
    			{
    			laserRay.Scale = new vec3(laserRay.Scale.x, MathLib.Length(intersection.Point - p0) + rayOffset, laserRay.Scale.z);
    			laserHit.WorldPosition = intersection.Point;
    			laserHit.Enabled = true;
    			}
    			else
    			{
    			laserHit.Enabled = false;
    			}
    
    			if (hitObj != null)
    			{
    			objText.Enabled = true;
    			objText.Text = hitObj.Name;
    			float radius = objText.BoundSphere.Radius;
    			vec3 shift = vec3.UP * radius;
    			objText.WorldTransform = MathLib.SetTo(laserHit.WorldPosition + shift, VRPlayer.LastPlayer.HeadController.WorldPosition, vec3.UP, MathLib.AXIS.Z);
    				
    			// реализация логики альтернативного действия (циклическая смена материала на объекте по списку)
    			if(altuse)
    			{
    				current_material++;
    				if (current_material >= materials.Capacity)
    					current_material = 0;
    
    					hitObj.SetMaterial(materials[current_material], 0);
    				Log.Message("ALTUSE HIT\n");
    				altuse = false;
    			}
    			}
    			else
    			objText.Enabled = false;
    		}
    	}
    
    	public override void OnGrabBegin(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		grabbed = true;
    	}
    
    	public override void OnGrabEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		grabbed = false;
    		laser.Enabled = false;
    		objText.Enabled = false;
    	}
    
    	public override void OnUseBegin(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		if(grabbed)
    			laser.Enabled = true;
    	}
    
    	public override void OnUseEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		laser.Enabled = false;
    		objText.Enabled = false;
    	}
    
    	// перегрузка метода, вызываемого при выполнении альтернативного действия с объектом
    	public override void OnAltUse(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		altuse = true;
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.ALT_USED;
    	}
    
    	// перегрузка метода, вызываемого при завершении альтернативного действия с объектом
    	public override void OnAltUseEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		altuse = false;
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.NOT_INTERACT;
    	}
    }
  3. Add an execution condition for the OnAltUse action. All available types of interactions are implemented in the VRHandShapeInteraction component (vr_template/components/interactions/interactions/VRHandShapeInteraction.cs). So, add the following code to the Interact() method of this component:

    VRHandShapeInteraction.cs

    Исходный код (C#)
    public override void Interact(VRInteractionManager.InteractablesState interactablesState, float ifps)
    {
    	// ...
    
    	// update current input
    	bool grabDown = false;
    	bool grabUp = false;
    	bool useDown = false;
    	bool useUp = false;
    	// reset flags before checking the current input
    	bool altUse = false;
    	bool altUseUp = false;
    
    
    	switch (controller.Device)
    	{
    		case VRInput.VRDevice.LEFT_CONTROLLER:
    			grabDown = VRInput.IsLeftButtonDown(VRInput.ControllerButtons.GRAB_BUTTON);
    			grabUp = VRInput.IsLeftButtonUp(VRInput.ControllerButtons.GRAB_BUTTON);
    			useDown = VRInput.IsLeftButtonDown(VRInput.ControllerButtons.USE_BUTTON);
    			useUp = VRInput.IsLeftButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			// for the left controller, the Use button must be hold on the right controller
    			altUse = VRInput.IsRightButtonPress(VRInput.ControllerButtons.USE_BUTTON);  
    			altUseUp = VRInput.IsRightButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			break;
    
    		case VRInput.VRDevice.RIGHT_CONTROLLER:
    			grabDown = VRInput.IsRightButtonDown(VRInput.ControllerButtons.GRAB_BUTTON);
    			grabUp = VRInput.IsRightButtonUp(VRInput.ControllerButtons.GRAB_BUTTON);
    			useDown = VRInput.IsRightButtonDown(VRInput.ControllerButtons.USE_BUTTON);
    			useUp = VRInput.IsRightButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			// for the right controller, the Use button must be hold on the left controller
    			altUse = VRInput.IsLeftButtonPress(VRInput.ControllerButtons.USE_BUTTON);
    			altUseUp = VRInput.IsLeftButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			break;
    
    		case VRInput.VRDevice.PC_HAND:
    			grabDown = VRInput.IsGeneralButtonDown(VRInput.GeneralButtons.FIRE_1);
    			grabUp = VRInput.IsGeneralButtonUp(VRInput.GeneralButtons.FIRE_1);
    			useDown = VRInput.IsGeneralButtonDown(VRInput.GeneralButtons.FIRE_2);
    			useUp = VRInput.IsGeneralButtonUp(VRInput.GeneralButtons.FIRE_2);
    			// for the keyboard, the JUMP button must be held
    			altUse = VRInput.IsGeneralButtonDown(VRInput.GeneralButtons.JUMP);
    			altUseUp = VRInput.IsGeneralButtonUp(VRInput.GeneralButtons.JUMP);
    			break;
    
    		default: break;
    	}
    
    	// stop the "alternative use" action - call OnAltUseEnd
    	// for all components of the hovered object that have an implementation of this method
    	if (altUseUp)
    	{
    		foreach (var hoveredObjectComponent in hoveredObjectComponents)
    			hoveredObjectComponent.OnAltUseEnd(this, controller);
    	}
    
    	// can grab and use hovered object
    	if (hoveredObject != null)
    	{
    		if (grabDown && grabbedObject == null)
    		{
    			grabbedObject = hoveredObject;
    			grabbedObjectComponents.Clear();
    			grabbedObjectComponents.AddRange(hoveredObjectComponents);
    			foreach (var grabbedObjectComponent in grabbedObjectComponents)
    			{
    			if (VRInteractionManager.IsGrabbed(grabbedObjectComponent))
    			{
    				VRBaseInteraction inter = VRInteractionManager.GetGrabInteraction(grabbedObjectComponent);
    				VRInteractionManager.StopGrab(inter);
    			}
    
    			grabbedObjectComponent.OnGrabBegin(this, controller);
    			interactablesState.SetGrabbed(grabbedObjectComponent, true, this);
    			}
    		}
    
    		if (useDown)
    		{
    			usedObject = hoveredObject;
    			usedObjectComponents.Clear();
    			usedObjectComponents.AddRange(hoveredObjectComponents);
    			foreach (var usedObjectComponent in usedObjectComponents)
    			{
    				usedObjectComponent.OnUseBegin(this, controller);
    				interactablesState.SetUsed(usedObjectComponent, true, this);
    			}
    		}
    		// call OnAltUse for all components of the hovered object that have an implementation of this method
    		if (altUse)
    		{
    		foreach (var hoveredObjectComponent in hoveredObjectComponents)
    			if (hoveredObjectComponent.CurrentState != VRBaseInteractable.INTERACTABLE_STATE.ALT_USED)
    				hoveredObjectComponent.OnAltUse(this, controller);
    		altUse = false;
    		}
    	}
    
    	// ...
    }
    Далее добавьте условие, при котором выполняется действие OnAltUse. Все возможные для интерактивных объектов виды взаимодействий реализованы в компоненте VRHandShapeInteraction (vr_template/components/interactions/interactions/VRHandShapeInteraction.cs). В этот файл и надо добавить немного кода в метод Interact():

    VRHandShapeInteraction.cs

    Исходный код (C#)
    public override void Interact(VRInteractionManager.InteractablesState interactablesState, float ifps)
    {
    	// ...
    
    	// update current input
    	bool grabDown = false;
    	bool grabUp = false;
    	bool useDown = false;
    	bool useUp = false;
    	// сброс флагов перед проверкой текущего ввода
    	bool altUse = false;
    	bool altUseUp = false;
    
    
    	switch (controller.Device)
    	{
    		case VRInput.VRDevice.LEFT_CONTROLLER:
    			grabDown = VRInput.IsLeftButtonDown(VRInput.ControllerButtons.GRAB_BUTTON);
    			grabUp = VRInput.IsLeftButtonUp(VRInput.ControllerButtons.GRAB_BUTTON);
    			useDown = VRInput.IsLeftButtonDown(VRInput.ControllerButtons.USE_BUTTON);
    			useUp = VRInput.IsLeftButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			// для левого контроллера должна удерживаться кнопка использования на правом  
    			altUse = VRInput.IsRightButtonPress(VRInput.ControllerButtons.USE_BUTTON);  
    			altUseUp = VRInput.IsRightButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			break;
    
    		case VRInput.VRDevice.RIGHT_CONTROLLER:
    			grabDown = VRInput.IsRightButtonDown(VRInput.ControllerButtons.GRAB_BUTTON);
    			grabUp = VRInput.IsRightButtonUp(VRInput.ControllerButtons.GRAB_BUTTON);
    			useDown = VRInput.IsRightButtonDown(VRInput.ControllerButtons.USE_BUTTON);
    			useUp = VRInput.IsRightButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			// для правого контроллера должна удерживаться кнопка использования на левом
    			altUse = VRInput.IsLeftButtonPress(VRInput.ControllerButtons.USE_BUTTON);
    			altUseUp = VRInput.IsLeftButtonUp(VRInput.ControllerButtons.USE_BUTTON);
    			break;
    
    		case VRInput.VRDevice.PC_HAND:
    			grabDown = VRInput.IsGeneralButtonDown(VRInput.GeneralButtons.FIRE_1);
    			grabUp = VRInput.IsGeneralButtonUp(VRInput.GeneralButtons.FIRE_1);
    			useDown = VRInput.IsGeneralButtonDown(VRInput.GeneralButtons.FIRE_2);
    			useUp = VRInput.IsGeneralButtonUp(VRInput.GeneralButtons.FIRE_2);
    			// для клавиатуры должна удерживаться кнопка JUMP (Прыжок)
    			altUse = VRInput.IsGeneralButtonDown(VRInput.GeneralButtons.JUMP);
    			altUseUp = VRInput.IsGeneralButtonUp(VRInput.GeneralButtons.JUMP);
    			break;
    
    		default: break;
    	}
    
    	// прекращение альтернативного использования - вызываем 'OnAltUseEnd'
    	// для всех компонент на выделенном (hovered) объекте, для которых он реализован
    	if (altUseUp)
    	{
    		foreach (var hoveredObjectComponent in hoveredObjectComponents)
    			hoveredObjectComponent.OnAltUseEnd(this, controller);
    	}
    
    	// can grab and use hovered object
    	if (hoveredObject != null)
    	{
    		if (grabDown && grabbedObject == null)
    		{
    			grabbedObject = hoveredObject;
    			grabbedObjectComponents.Clear();
    			grabbedObjectComponents.AddRange(hoveredObjectComponents);
    			foreach (var grabbedObjectComponent in grabbedObjectComponents)
    			{
    				if (VRInteractionManager.IsGrabbed(grabbedObjectComponent))
    				{
    				VRBaseInteraction inter = VRInteractionManager.GetGrabInteraction(grabbedObjectComponent);
    				VRInteractionManager.StopGrab(inter);
    				}
    
    				grabbedObjectComponent.OnGrabBegin(this, controller);
    				interactablesState.SetGrabbed(grabbedObjectComponent, true, this);
    			}
    		}
    
    		if (useDown)
    		{
    			usedObject = hoveredObject;
    			usedObjectComponents.Clear();
    			usedObjectComponents.AddRange(hoveredObjectComponents);
    			foreach (var usedObjectComponent in usedObjectComponents)
    			{
    				usedObjectComponent.OnUseBegin(this, controller);
    				interactablesState.SetUsed(usedObjectComponent, true, this);
    			}
    		}
    		// при выполнении условий альтернативного использования вызываем соответствующий метод (OnAltUse)
    		// для всех компонент на выделенном (hovered) объекте, для которых этот метод реализован
    		if (altUse)
    		{
    		foreach (var hoveredObjectComponent in hoveredObjectComponents)
    			if (hoveredObjectComponent.CurrentState != VRBaseInteractable.INTERACTABLE_STATE.ALT_USED)
    				hoveredObjectComponent.OnAltUse(this, controller);
    			altUse = false;
    		}
    	}
    
    	// ...
    }
  4. Specify the list of materials that will be applied to the object. Select the laser_pointer node and click Edit in the Parameters window. Then find the VRLaserPointer component, specify the number of elements for the Materials array (for example, 3) and drag the required materials from the Materials window to fill the array.

    Заполните список материалов для переключения. Для этого выделите ноду laser_pointer и нажмите Edit. В компоненте VRLaserPointer для массива materials задайте количество элементов (например, 3) и перетащите подходящие материалы из окна Materials, заполняя ячейки массива.

  5. Save changes (Ctrl+S) and press the Play button to run the application.Сохраните мир (Ctrl+S) и нажмите кнопку Play.

3.7. Customizing Menu
3.6. Кастомизация меню.
#

В шаблоне VR-приложения имеется меню двух основных типов: одно привязано к положению шлема, а второе – к руке. Для основного меню в сэмпле используется нода MenuAttachedToHead (ObjectGui), а для второго – ноды menu (ObjectGui), добавленные, как дочерние к соответствующим контроллерам (например, vive_left_controller).

All menu nodes have the same VRMenuSample.cs component assigned. It is inherited from the base VRBaseUI component that implements a user interface. In general, the user interface is implemented as follows:В сэмпле для всех меню используется один компонент VRMenuSample.cs. Он наследуется от базового класса для реализации всех пользовательских интерфейсов – VRBaseUI. Процесс реализации интерфейса в общем случае выглядит так:

  1. Inherit a new component from the base VRBaseUI.Наследуем новый компонент от базового VRBaseUI.
  2. Implement the user interface with the required widgets in the overridden InitGui() method.В перегрузке метода InitGui() добавляем код генерации пользовательского интерфейса с нужным набором виджетов.
  3. Пишем функции обратного вызова (колбэки) для обработки событий виджетов (нажатие на кнопку, выбор элементов в выпадающем меню и т.д.).
  4. Добавляем колбэки к соответствующим виджетам в InitGui().
  5. Assign the component to the ObjectGui node that will display the user interface.Назначаем компонент на ноду ObjectGui, на которой будет отображаться создаваемый интерфейс.

Let's create a new menu for the left controller and add the quit button:Давайте сделаем отдельное меню для левого контроллера и добавим в него кнопку выхода из приложения:

  1. Inherit a new component from the base VRBaseUI, call it VRHandMenu and copy the following code to it:

    VRHandMenu.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- an identifier is generated automatically for the component
    public class VRHandMenu : VRBaseUI
    {
    	// declare required widgets
    	private WidgetSprite background;
    	private WidgetVBox VBox;
    	private WidgetButton quitButton;
    	private WidgetWindow window;
    	private WidgetButton okButton;
    	private WidgetButton cancelButton;
    	private WidgetHBox HBox;
    
    	// overriden method that is called on interface initialization,
    	// it adds the required widgets and subscribes to events
    	protected override void InitGui()
    	{
    		if (gui == null)
    		return;
    
    		background= new WidgetSprite(gui, "core/textures/common/black.texture");
    		background.Color = new vec4(1.0f, 1.0f, 1.0f, 0.5f);
    		gui.AddChild(background, Gui.ALIGN_BACKGROUND | Gui.ALIGN_EXPAND);
    		
    		// add a vertical column container WidgetVBox to GUI
    		VBox = new WidgetVBox();
    		VBox.Width = gui.Width;
    		VBox.Height = 100;
    		gui.AddChild(VBox, Gui.ALIGN_OVERLAP|Gui.ALIGN_CENTER);
    
    		// add the quit button to the container
    		quitButton = new WidgetButton(gui, "QUIT APPLICATION");
    		quitButton.FontSize = 20;
    		VBox.AddChild(quitButton);
    		quitButton.EventClicked.Connect(ButtonQuitClicked);
    		VBox.Arrange();
    
    		// create a quit confirmation window
    		window = new WidgetWindow(gui, "Quit Application");
    		window.FontSize = 20;
    		
    		// add a horizontal row container WidgetHBox to the confirmation window
    		HBox = new WidgetHBox();
    		HBox.Width = window.Width;
    		HBox.Height = 100;
    		window.AddChild(HBox);
    
    		// add the "OK" button and assign the "OkClicked" handler that will close the application
    		okButton = new WidgetButton(gui, "OK");
    		okButton.FontSize = 20;
    		HBox.AddChild(okButton);
    		okButton.EventClicked.Connect(OkClicked);
    
    		// add the "Cancel" button and assign the "OkClicked" handler that will close the confirmation window
    		cancelButton = new WidgetButton(gui, "Cancel");
    		cancelButton.FontSize = 20;
    		HBox.AddChild(cancelButton);
    		cancelButton.EventClicked.Connect(CancelClicked);
    	}
    
    	// function to be executed on clicking "quitButton"
    	private void ButtonQuitClicked()
    	{
    		gui.AddChild(window, Gui.ALIGN_OVERLAP | Gui.ALIGN_CENTER);
    	}
    
    	// function to be executed on clicking "cancelButton"
    	private void CancelClicked()
    	{
    		gui.RemoveChild(window);
    	}
    
    	// function to be executed on clicking "okButton"
    	private void OkClicked()
    	{
    		// quit the application
    		Engine.Quit();
    	}
    
    }
    Создайте новый компонент (наследник VRBaseUI), назовите его VRHandMenu и скопируйте в него следующий код:

    VRHandMenu.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
    public class VRHandMenu : VRBaseUI
    {
    	// объявляем нужные виджеты
    	private WidgetSprite background;
    	private WidgetVBox VBox;
    	private WidgetButton quitButton;
    	private WidgetWindow window;
    	private WidgetButton okButton;
    	private WidgetButton cancelButton;
    	private WidgetHBox HBox;
    
    	// перегрузка метода, вызываемого при инициализации интерфейса,
    	// здесь добавляются все нужные виджеты и назначаются колбэки
    	protected override void InitGui()
    	{
    		if (gui == null)
    			return;
    
    		background= new WidgetSprite(gui, "core/textures/common/black.texture");
    		background.Color = new vec4(1.0f, 1.0f, 1.0f, 0.5f);
    		gui.AddChild(background, Gui.ALIGN_BACKGROUND | Gui.ALIGN_EXPAND);
    		
    		// добавляем в GUI контейнер WidgetVBox с вертикальным расположением виджетов
    		VBox = new WidgetVBox();
    		VBox.Width = gui.Width;
    		VBox.Height = 100;
    		gui.AddChild(VBox, Gui.ALIGN_OVERLAP|Gui.ALIGN_CENTER);
    
    		// добавляем в контейнер кнопку, отображающую окно подтверждения выхода из приложения
    		quitButton = new WidgetButton(gui, "QUIT APPLICATION");
    		quitButton.FontSize = 20;
    		VBox.AddChild(quitButton);
    		quitButton.AddCallback(Gui.CALLBACK_INDEX.CLICKED, ButtonQuitClicked);
    		VBox.Arrange();
    
    		// создаем окно подтверждения выхода из приложения
    		window = new WidgetWindow(gui, "Quit Application");
    		window.FontSize = 20;
    		
    		// добавляем в окно контейнер WidgetHBox с горизонтальным расположением виджетов
    		HBox = new WidgetHBox();
    		HBox.Width = window.Width;
    		HBox.Height = 100;
    		window.AddChild(HBox);
    
    		// добавляем в контейнер кнопку 'OK' c колбеком 'OkClicked', закрывающим приложение
    		okButton = new WidgetButton(gui, "OK");
    		okButton.FontSize = 20;
    		HBox.AddChild(okButton);
    		okButton.AddCallback(Gui.CALLBACK_INDEX.CLICKED, OkClicked);
    
    		// добавляем в контейнер кнопку 'Cancel' c колбэком 'OkClicked', закрывающим окно подтверждения
    		cancelButton = new WidgetButton(gui, "Cancel");
    		cancelButton.FontSize = 20;
    		HBox.AddChild(cancelButton);
    		cancelButton.AddCallback(Gui.CALLBACK_INDEX.CLICKED, CancelClicked);   	 
    	}
    
    	// функция, вызываемая при нажатии на кнопку 'quitButton'
    	private void ButtonQuitClicked()
    	{
    		gui.AddChild(window, Gui.ALIGN_OVERLAP | Gui.ALIGN_CENTER);
    	}
    
    	// функция, вызываемая при нажатии на кнопку 'cancelButton'
    	private void CancelClicked()
    	{
    		gui.RemoveChild(window);
    	}
    
    	// функция, вызываемая при нажатии на кнопку 'okButton'
    	private void OkClicked()
    	{
    		// закрываем приложение
    		Engine.Quit();
    	}
    
    }
  2. Назначьте компонент VRHandMenu.cs на ноду menu (это дочерняя нода vive_left_controller).

  3. Save changes (Ctrl+S) and click Play to run the application.Сохраните мир (Ctrl+S) и нажмите кнопку Play.

3.8. Adding a New Interactable Object
3.7. Добавление нового интерактивного объекта
#

Let's add a new type of interactable object that we can grab, hold, and throw (i.e. it will be inherited from the VRBaseInteractable component) with an additional feature: some visual effect (for example, smoke) will appear when we grab the object and, it will disappear when we release the object. In addition, it will display certain text in the console, if the corresponding option is enabled.Давайте добавим новый тип интерактивного объекта, который мы также сможем захватывать, удерживать и бросать (т.е. это будет наследник VRBaseInteractable) с дополнительной функцией: появление визуального эффекта (например, пара) при захвате и исчезновение эффекта при отпускании. Также будем отображать определенный текст в консоли, если соответствующая опция включена.

  1. Create a new VRObjectVFX component and add the following code to it:

    VRObjectVFX.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- an identifier is generated automatically for the component
    public class VRObjectVFX : VRBaseInteractable
    {
    	// an asset with an effect that occurs when the object is grabbed
    	public AssetLinkNode vfx_node = null;
    	private Node vfx = null;
    	private Unigine.Object obj = null;
    	
    	// this method is called for the interactable object each frame
    	private void Update()
    	{
    		// update transformation of the effect if the object is grabbed
    		if(CurrentState == VRBaseInteractable.INTERACTABLE_STATE.GRABBED)
    	vfx.WorldTransform = node.WorldTransform;
    	}
    
    	// this overridden method is called after component initialization
    	// (you can implement checks here)
    	protected override void OnReady()
    	{
    	// check if the asset with the visual effect is specified	
    	if (vfx_node == null)
    	{
    		Log.Error($"{nameof(VRObjectVFX)} error: 'vfx_node' is not assigned.\n");
    		Enabled = false;
    		return;
    	}
    	else
    	{
    			vfx = vfx_node.Load();
    			vfx.Enabled = false;
    	}
    	
    	}
    
    	// this overridden method is called when grabbing the interactable object
    	public override void OnGrabBegin(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		// show the effect
    		vfx.Enabled = true;
    		// set the current object state to GRABBED
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.GRABBED;
    	}
    
    	// this overridden method is called when releasing the interactable object
    	public override void OnGrabEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		// hide the effect
    		vfx.Enabled = false;
    		// set the current object state to NOT_INTERACT
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.NOT_INTERACT;
    	}
    }
    Создайте новый компонент, назовите его VRObjectVFX и скопируйте в него следующий код:

    VRObjectVFX.cs

    Исходный код (C#)
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using Unigine;
    
    [Component(PropertyGuid = "AUTOGEN_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
    public class VRObjectVFX : VRBaseInteractable
    {
    	// ассет с эффектом, который должен отображаться при захвате объекта
    	public AssetLinkNode vfx_node = null;
    	private Node vfx = null;
    	private Unigine.Object obj = null;
    	
    	// метод, вызываемый для интерактивного объекта каждый кадр
    	private void Update()
    	{
    		// если объект захвачен, обновляем положение эффекта
    		if(CurrentState == VRBaseInteractable.INTERACTABLE_STATE.GRABBED)
    		vfx.WorldTransform = node.WorldTransform;
    	}
    
    	// перегрузка метода, вызываемого по готовности после инициализации компонента 
    	// (здесь удобно выполнять проверки)
    	protected override void OnReady()
    	{
    	// проверяем, указан ли ассет для загрузки визуального эффекта	
    	if (vfx_node == null)
    	{
    			Log.Error($"{nameof(VRObjectVFX)} error: 'vfx_node' is not assigned.\n");
    			Enabled = false;
    			return;
    	}
    	else
    	{
    			vfx = vfx_node.Load();
    			vfx.Enabled = false;
    	}
    	
    	}
    
    	// перегрузка метода, вызываемого при захватывании интерактивного объекта
    	public override void OnGrabBegin(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		// показываем эффект
    		vfx.Enabled = true;
    		// устанавливаем текущее состояние 'GRABBED'
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.GRABBED;
    	}
    
    	// перегрузка метода, вызываемого при отпускании интерактивного объекта
    	public override void OnGrabEnd(VRBaseInteraction interaction, VRBaseController controller)
    	{
    		// скрываем эффект
    		vfx.Enabled = false;
    		// устанавливаем текущее состояние 'NOT_INTERACT'
    		CurrentState = VRBaseInteractable.INTERACTABLE_STATE.NOT_INTERACT;
    	}
    }
  2. Assing the VRObjectVFX.cs component to the cylinder node (the child of the kinematic_movable dummy node).Назначьте компонент VRObjectVFX.cs на ноду cylinder (это дочерняя нода NodeDummy с именем kinematic_movable).
  3. Drag the vr/particles/smoke.node asset to the Vfx Node field. This node stores the particle system representing the smoke effect. It is available in the vr/particles folder of UNIGINE Starter Course Projects add-on.

    В поле Vfx Node перетащите ассет vr/particles/smoke.node, содержащий систему частиц с эффектом пара. Он хранится в папке vr/particles аддона UNIGINE Starter Course Project.

  4. Save changes (Ctrl+S) and press the Play button to run the application.Сохраните мир (Ctrl+S) и нажмите кнопку Play.

Now if you grab and hold the cylinder, it will emit smoke:Теперь при захвате и удержании цилиндра от него будет идти пар:

4. Packing a Final Build
4. Сборка финального билда проекта.
#

Now, you can build the project. Specify the required settings and ensure the final build includes all content, code, and necessary libraries. Ensure all unused assets are deleted. And don't forget to enable the VR modes!Теперь, когда все готово, можно собрать проект. Установите нужные параметры, убедитесь в том, что весь нужный контент включен в сборку, а все лишнее убрано. И не забудьте включить VR режимы!

To get more information on how to pack the final build, refer to the Programming Quick Start section.Процесс сборки финального билда подробно описан в разделе Быстрый старт для программистов.

Where to Go From Here
Что дальше?
#

Congratulations! Now, you can continue developing the project on your own. Here are some helpful recommendations for you: Поздравляем! Вы создали простой VR-проект. Теперь вы можете продолжить разработку самостоятельно. Вот несколько рекомендаций, которые могут быть полезны:

  • Try to analyze the source code of the sample further to figure out how it works and use it as a reference to write your own code. Попытайтесь проанализировать исходный код примера и выяснить, как он работает, используйте его, чтобы написать свой собственный.
  • Read the Virtual Reality Best Practices article for more information and tips on preparing content for VR and enhancing the user experience. Прочтите статью Virtual Reality Best Practices для получения дополнительной информации и полезных советов по подготовке контента для VR и улучшению взаимодействия с пользователем.
  • Learn more about the Component System by reading the Component System article. Прочтите статью Component System для получения дополнительной информации о работе с Компонентной системой.
  • Check out the Component System Usage Example for more details on implementing logic using the Component System. Ознакомьтесь с Примером использования компонентной системы для получения более подробной информации о реализации логики с использованием системы компонентов.
Последнее обновление: 19.04.2024
Build: ()