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 } }