using System;
using System.Collections.Generic;
#if UNITY_EDITOR
using System.Linq;
using UnityEditor;
using UnityEditor.XR.Management;
using UnityEngine.XR.Management;
#endif
namespace UnityEngine.XR.OpenXR.Features
{
///
/// A Unity OpenXR Interaction feature.
/// This class can be inherited from to add a custom action mapping for OpenXR.
///
[Serializable]
public abstract class OpenXRInteractionFeature : OpenXRFeature
{
///
/// Temporary static list used for action map creation
///
private static List m_CreatedActionMaps = null;
private static Dictionary> m_InteractionProfileEnabledMaps = new Dictionary>();
///
/// Flag that indicates this feature or profile is additive and its binding paths will be added to other non-additive profiles if enabled.
///
internal virtual bool IsAdditive => false;
///
/// The underlying type of an OpenXR action. This enumeration contains all supported control types within OpenXR. This is used when declaring actions in OpenXR with XrAction/>.
///
[Serializable]
protected internal enum ActionType
{
/// A binary (on/off) action type. Represented by ButtonControl in the Input System or Boolean in XR.InputDevice.
Binary,
/// A single Axis float action type. Represented by an AxisControl in the InputSystem or a float in XR.InputDevice.
Axis1D,
/// A two-dimensional float action type. Represented by a Vector2Control in the InputSystem or Vector2 in XR.InputDevice.
Axis2D,
/// A position and rotation in three-dimensional space. Represented by a PoseControl in the InputSystem, and a series of controls (boolean to represent if it's being tracked or not, unsigned integer for which fields are available, Vector3 for position, Quaternion for rotation) in XR.InputDevice.
Pose,
/// This control represents an output motor. Usable as sequential channels (first declared is channel 0, second is 1, etc...) in both the Input System and XR.InputDevice haptic APIs.
Vibrate,
/// A value representing the total number of ActionTypes available. This can be used to check if an ActionType value is a valid ActionType.
Count
}
///
/// Information sent to OpenXR about specific, physical control on an input device. Used to identify what an action is bound to (that is, which physical control will trigger that action).
///
[Serializable]
protected internal class ActionBinding
{
/// OpenXR interaction profile name
public string interactionProfileName;
/// OpenXR path for the interaction
public string interactionPath;
/// Optional OpenXR user paths
public List userPaths;
}
///
/// Declares an abstract input source bound to multiple physical input controls. XrActions are commonly contained within an ActionMapConfig as a grouped series of abstract, remappable inputs.
///
[Serializable]
protected internal class ActionConfig
{
/// The name of the action, reported into the InputSystem as the name of the control that represents the input data for this action. This name can only contain a-z lower case letters.
public string name;
/// The type of data this action will report.
public ActionType type;
/// Human readable name for the action
public string localizedName;
/// The underlying physical input controls to use as the value for this action
public List bindings;
/// These will be tagged onto features. See
public List usages;
/// Tag to determine if certain action is additive and could be added to the existing profiles
public bool isAdditive;
}
///
/// Information sent to the OpenXR runtime to identify how to map a or to an underlying OpenXR user path.
///
protected internal class DeviceConfig
{
/// The for the that will represent this ActionMapConfig. See
public InputDeviceCharacteristics characteristics;
/// OpenXR user path that this device maps to.
public string userPath;
}
///
/// Defines a mapping of between the Unity input system and OpenXR.
///
[Serializable]
protected internal class ActionMapConfig
{
///
/// Name of the action map
///
public string name;
///
/// Human readable name of the OpenXR user path that this device maps to.
///
public string localizedName;
///
/// List of devices to configure
///
public List deviceInfos;
///
/// List of actions to configure
///
public List actions;
///
/// OpenXR interaction profile to use for this action map
///
public string desiredInteractionProfile;
///
/// Name of the manufacturer providing this action map
///
public string manufacturer;
///
/// Serial number of the device
///
public string serialNumber;
}
///
/// Common OpenXR user path definitions.
/// See the [OpenXR Specification](https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#semantic-path-user) for more information.
///
public static class UserPaths
{
///
/// Path for user left hand
///
public const string leftHand = "/user/hand/left";
///
/// Path for user right hand
///
public const string rightHand = "/user/hand/right";
///
/// Path for user head
///
public const string head = "/user/head";
///
/// Path for user gamepad
///
public const string gamepad = "/user/gamepad";
///
/// Path for user treadmill
///
public const string treadmill = "/user/treadmill";
}
///
/// Flags used to indicate Interaction profile type
///
public enum InteractionProfileType
{
///
/// Interaction profile derived from InputDevice class
///
Device,
///
/// Interaction profile derived from XRController class
///
XRController
}
///
/// Register a device layout with the Unity Input System.
/// Called whenever this interaction profile is enabled in the Editor.
///
protected virtual void RegisterDeviceLayout()
{
}
///
/// Remove a device layout from the Unity Input System.
/// Called whenever this interaction profile is disabled in the Editor.
///
protected virtual void UnregisterDeviceLayout()
{
}
///
/// Register action maps for this device with the OpenXR Runtime.
/// Called at runtime before Start.
///
protected virtual void RegisterActionMapsWithRuntime()
{
}
///
protected internal override bool OnInstanceCreate(ulong xrSession)
{
RegisterDeviceLayout();
return true;
}
///
/// Return interaction profile type. Default type is XRController. Override this if interactionProfile is not derived from XRController class.
///
/// Interaction profile type.
protected virtual InteractionProfileType GetInteractionProfileType() => InteractionProfileType.XRController;
///
/// Return device layout name string used for register layouts in inputSystem.
///
/// Device layout string.
protected virtual string GetDeviceLayoutName() => "";
///
/// Request the feature create its action maps
///
/// Target list for the action maps
internal void CreateActionMaps(List configs)
{
m_CreatedActionMaps = configs;
RegisterActionMapsWithRuntime();
m_CreatedActionMaps = null;
}
///
/// Add an action map to the Unity Input System.
///
/// This method must be called from within the RegisterActionMapsWithRuntime method.
///
/// Action map to add
protected void AddActionMap(ActionMapConfig map)
{
if (null == map)
throw new ArgumentNullException("map");
if (null == m_CreatedActionMaps)
throw new InvalidOperationException("ActionMap must be added from within the RegisterActionMapsWithRuntime method");
m_CreatedActionMaps.Add(map);
}
internal virtual void AddAdditiveActions(List actionMaps, ActionMapConfig additiveMap)
{
}
///
/// Handle enabled state change
///
protected internal override void OnEnabledChange()
{
base.OnEnabledChange();
#if UNITY_EDITOR && INPUT_SYSTEM_BINDING_VALIDATOR
var packageSettings = OpenXRSettings.GetSettingsForBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
if (null == packageSettings)
return;
foreach (var feature in packageSettings.GetFeatures())
{
var profileType = ((OpenXRInteractionFeature) feature).GetInteractionProfileType();
string deviceLayoutName = ((OpenXRInteractionFeature) feature).GetDeviceLayoutName();
deviceLayoutName = "<" + deviceLayoutName + ">";
if (m_InteractionProfileEnabledMaps.ContainsKey(profileType) && m_InteractionProfileEnabledMaps[profileType].ContainsKey(deviceLayoutName))
m_InteractionProfileEnabledMaps[profileType][deviceLayoutName] = feature.enabled;
}
#endif
}
internal static void RegisterLayouts()
{
#if ENABLE_INPUT_SYSTEM
#if UNITY_EDITOR
var packageSettings = OpenXRSettings.GetSettingsForBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
if (null == packageSettings)
return;
#if INPUT_SYSTEM_BINDING_VALIDATOR
m_InteractionProfileEnabledMaps.Clear();
foreach (var feature in packageSettings.GetFeatures())
{
//Register all the available profiles
((OpenXRInteractionFeature) feature).RegisterDeviceLayout();
var profileType = ((OpenXRInteractionFeature) feature).GetInteractionProfileType();
string deviceLayoutName = ((OpenXRInteractionFeature) feature).GetDeviceLayoutName();
if (String.IsNullOrEmpty(deviceLayoutName))
{
Debug.LogWarningFormat("No GetDeviceLayoutName() override detected in {0}. Binding path validator for this interaction profile is not as effective. To fix, add GetDeviceLayoutName and GetInteractionProfileType override in this profile.", feature.nameUi);
continue;
}
deviceLayoutName = "<" + deviceLayoutName + ">";
if (!m_InteractionProfileEnabledMaps.ContainsKey(profileType))
m_InteractionProfileEnabledMaps[profileType] = new Dictionary();
m_InteractionProfileEnabledMaps[profileType].Add(deviceLayoutName, feature.enabled);
}
InputSystem.InputSystem.customBindingPathValidators -= PathValidator;
InputSystem.InputSystem.customBindingPathValidators += PathValidator;
#else //#if INPUT_SYSTEM_BINDING_VALIDATOR
foreach (var feature in packageSettings.GetFeatures())
{
//Register all the available profiles
((OpenXRInteractionFeature)feature).RegisterDeviceLayout();
}
#endif //#if INPUT_SYSTEM_BINDING_VALIDATOR
#else
foreach (var feature in OpenXRSettings.Instance.GetFeatures())
if (feature.enabled)
((OpenXRInteractionFeature)feature).RegisterDeviceLayout();
#endif //#if UNITY_EDITOR
#endif //#if ENABLE_INPUT_SYSTEM
}
#if UNITY_EDITOR && INPUT_SYSTEM_BINDING_VALIDATOR
internal static Action PathValidator(string bindingPath)
{
//case1: OpenXR plugin not enabled in XR management
if (!OpenXRLoaderEnabledForSelectedBuildTarget(EditorUserBuildSettings.selectedBuildTargetGroup))
return null;
string warningText = null;
//case2: current bindingPath maps to XRController.
if (bindingPath.StartsWith(""))
{
if (!m_InteractionProfileEnabledMaps.ContainsKey(InteractionProfileType.XRController))
return null;
bool controllerProfileEnabled = false;
foreach (var profile in m_InteractionProfileEnabledMaps[InteractionProfileType.XRController])
{
if (profile.Value)
controllerProfileEnabled = true;
}
if (controllerProfileEnabled)
return null;
warningText = "This binding will be inactive because there are no enabled OpenXR interaction profiles.";
}
else
{
//case3: current bindingPath maps to specific OpenXR interaction profile
//Only check for bindings that belongs to OpenXRInteractionFeature
bool checkXRInteractionBinding = false;
bool profileEnabled = false;
foreach (var map in m_InteractionProfileEnabledMaps)
{
foreach (var profile in map.Value)
{
if (bindingPath.StartsWith(profile.Key, StringComparison.OrdinalIgnoreCase))
{
checkXRInteractionBinding = true;
profileEnabled = profile.Value;
break;
}
}
if (checkXRInteractionBinding)
break;
}
if (!checkXRInteractionBinding || profileEnabled)
return null;
warningText = "This binding will be inactive because it refers to a disabled OpenXR interaction profile.";
}
// Draw the warning information in the Binding Properties panel
return () =>
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
GUILayout.Label(EditorGUIUtility.IconContent("Warning@2x"), new GUIStyle(EditorStyles.label));
GUILayout.Label(warningText, EditorStyles.wordWrappedLabel);
EditorGUILayout.EndHorizontal();
if(GUILayout.Button("Manage Interaction Profiles"))
SettingsService.OpenProjectSettings("Project/XR Plug-in Management/OpenXR");
EditorGUILayout.Space();
EditorGUILayout.EndVertical();
};
}
#endif
#if UNITY_EDITOR
internal static bool OpenXRLoaderEnabledForSelectedBuildTarget(BuildTargetGroup targetGroup)
{
var settings =XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(targetGroup)?.AssignedSettings;
if (!settings)
return false;
bool loaderFound = false;
foreach (var activeLoader in settings.activeLoaders)
{
if (activeLoader as OpenXRLoader != null)
{
loaderFound = true;
break;
}
}
return loaderFound;
}
#endif
}
}