using System; using UnityEngine.InputSystem.LowLevel; namespace UnityEngine.InputSystem.XR { /// /// The component applies the current pose value of a tracked device /// to the of the . /// can track multiple types of devices including XR HMDs, controllers, and remotes. /// /// /// For and , if an action is directly defined /// in the , as opposed to a reference to an action externally defined /// in an , the action will automatically be enabled and disabled by this /// behavior during and . The enabled state for actions /// externally defined must be managed externally from this behavior. /// [Serializable] [AddComponentMenu("XR/Tracked Pose Driver (Input System)")] public class TrackedPoseDriver : MonoBehaviour, ISerializationCallbackReceiver { /// /// Options for which properties to update. /// /// public enum TrackingType { /// /// Update both rotation and position. /// RotationAndPosition, /// /// Update rotation only. /// RotationOnly, /// /// Update position only. /// PositionOnly, } /// /// These bit flags correspond with UnityEngine.XR.InputTrackingState /// but that enum is not used to avoid adding a dependency to the XR module. /// Only the Position and Rotation flags are used by this class, so velocity and acceleration flags are not duplicated here. /// [Flags] enum TrackingStates { /// /// Position and rotation are not valid. /// None, /// /// Position is valid. /// See InputTrackingState.Position. /// Position = 1 << 0, /// /// Rotation is valid. /// See InputTrackingState.Rotation. /// Rotation = 1 << 1, } [SerializeField, Tooltip("Which Transform properties to update.")] TrackingType m_TrackingType; /// /// The tracking type being used by the Tracked Pose Driver /// to control which properties to update. /// /// public TrackingType trackingType { get => m_TrackingType; set => m_TrackingType = value; } /// /// Options for which phases of the player loop will update properties. /// /// /// public enum UpdateType { /// /// Update after the Input System has completed an update and right before rendering. /// This is the recommended and default option to minimize lag for XR tracked devices. /// /// UpdateAndBeforeRender, /// /// Update after the Input System has completed an update except right before rendering. /// /// /// This may be dynamic update, fixed update, or a manual update depending on the Update Mode /// project setting for Input System. /// Update, /// /// Update after the Input System has completed an update right before rendering. /// /// /// Note that this update mode may not trigger if there are no XR devices added which use before render timing. /// /// /// BeforeRender, } [SerializeField, Tooltip("Updates the Transform properties after these phases of Input System event processing.")] UpdateType m_UpdateType = UpdateType.UpdateAndBeforeRender; /// /// The update type being used by the Tracked Pose Driver /// to control which phases of the player loop will update properties. /// /// public UpdateType updateType { get => m_UpdateType; set => m_UpdateType = value; } [SerializeField, Tooltip("Ignore Tracking State and always treat the input pose as valid.")] bool m_IgnoreTrackingState; /// /// Ignore tracking state and always treat the input pose as valid when updating the properties. /// The recommended value is so the tracking state input is used. /// /// public bool ignoreTrackingState { get => m_IgnoreTrackingState; set => m_IgnoreTrackingState = value; } [SerializeField, Tooltip("The input action to read the position value of a tracked device. Must be a Vector 3 control type.")] InputActionProperty m_PositionInput; /// /// The input action to read the position value of a tracked device. /// Must support reading a value of type . /// /// public InputActionProperty positionInput { get => m_PositionInput; set { if (Application.isPlaying) UnbindPosition(); m_PositionInput = value; if (Application.isPlaying && isActiveAndEnabled) BindPosition(); } } [SerializeField, Tooltip("The input action to read the rotation value of a tracked device. Must be a Quaternion control type.")] InputActionProperty m_RotationInput; /// /// The input action to read the rotation value of a tracked device. /// Must support reading a value of type . /// /// public InputActionProperty rotationInput { get => m_RotationInput; set { if (Application.isPlaying) UnbindRotation(); m_RotationInput = value; if (Application.isPlaying && isActiveAndEnabled) BindRotation(); } } [SerializeField, Tooltip("The input action to read the tracking state value of a tracked device. Identifies if position and rotation have valid data. Must be an Integer control type.")] InputActionProperty m_TrackingStateInput; /// /// The input action to read the tracking state value of a tracked device. /// Identifies if position and rotation have valid data. /// Must support reading a value of type . /// /// /// See [InputTrackingState](xref:UnityEngine.XR.InputTrackingState) enum for values the input action represents. /// /// /// [InputTrackingState.None](xref:UnityEngine.XR.InputTrackingState.None) (0) /// to indicate neither position nor rotation is valid. /// /// /// [InputTrackingState.Position](xref:UnityEngine.XR.InputTrackingState.Position) (1) /// to indicate position is valid. /// /// /// [InputTrackingState.Rotation](xref:UnityEngine.XR.InputTrackingState.Rotation) (2) /// to indicate rotation is valid. /// /// /// [InputTrackingState.Position](xref:UnityEngine.XR.InputTrackingState.Position) | [InputTrackingState.Rotation](xref:UnityEngine.XR.InputTrackingState.Rotation) (3) /// to indicate position and rotation is valid. /// /// /// /// public InputActionProperty trackingStateInput { get => m_TrackingStateInput; set { if (Application.isPlaying) UnbindTrackingState(); m_TrackingStateInput = value; if (Application.isPlaying && isActiveAndEnabled) BindTrackingState(); } } Vector3 m_CurrentPosition = Vector3.zero; Quaternion m_CurrentRotation = Quaternion.identity; TrackingStates m_CurrentTrackingState = TrackingStates.Position | TrackingStates.Rotation; bool m_RotationBound; bool m_PositionBound; bool m_TrackingStateBound; bool m_IsFirstUpdate = true; void BindActions() { BindPosition(); BindRotation(); BindTrackingState(); } void UnbindActions() { UnbindPosition(); UnbindRotation(); UnbindTrackingState(); } void BindPosition() { if (m_PositionBound) return; var action = m_PositionInput.action; if (action == null) return; action.performed += OnPositionPerformed; action.canceled += OnPositionCanceled; m_PositionBound = true; if (m_PositionInput.reference == null) { action.Rename($"{gameObject.name} - TPD - Position"); action.Enable(); } } void BindRotation() { if (m_RotationBound) return; var action = m_RotationInput.action; if (action == null) return; action.performed += OnRotationPerformed; action.canceled += OnRotationCanceled; m_RotationBound = true; if (m_RotationInput.reference == null) { action.Rename($"{gameObject.name} - TPD - Rotation"); action.Enable(); } } void BindTrackingState() { if (m_TrackingStateBound) return; var action = m_TrackingStateInput.action; if (action == null) return; action.performed += OnTrackingStatePerformed; action.canceled += OnTrackingStateCanceled; m_TrackingStateBound = true; if (m_TrackingStateInput.reference == null) { action.Rename($"{gameObject.name} - TPD - Tracking State"); action.Enable(); } } void UnbindPosition() { if (!m_PositionBound) return; var action = m_PositionInput.action; if (action == null) return; if (m_PositionInput.reference == null) action.Disable(); action.performed -= OnPositionPerformed; action.canceled -= OnPositionCanceled; m_PositionBound = false; } void UnbindRotation() { if (!m_RotationBound) return; var action = m_RotationInput.action; if (action == null) return; if (m_RotationInput.reference == null) action.Disable(); action.performed -= OnRotationPerformed; action.canceled -= OnRotationCanceled; m_RotationBound = false; } void UnbindTrackingState() { if (!m_TrackingStateBound) return; var action = m_TrackingStateInput.action; if (action == null) return; if (m_TrackingStateInput.reference == null) action.Disable(); action.performed -= OnTrackingStatePerformed; action.canceled -= OnTrackingStateCanceled; m_TrackingStateBound = false; } void OnPositionPerformed(InputAction.CallbackContext context) { m_CurrentPosition = context.ReadValue(); } void OnPositionCanceled(InputAction.CallbackContext context) { m_CurrentPosition = Vector3.zero; } void OnRotationPerformed(InputAction.CallbackContext context) { m_CurrentRotation = context.ReadValue(); } void OnRotationCanceled(InputAction.CallbackContext context) { m_CurrentRotation = Quaternion.identity; } void OnTrackingStatePerformed(InputAction.CallbackContext context) { m_CurrentTrackingState = (TrackingStates)context.ReadValue(); } void OnTrackingStateCanceled(InputAction.CallbackContext context) { m_CurrentTrackingState = TrackingStates.None; } /// /// This function is called when the user hits the Reset button in the Inspector's context menu /// or when adding the component the first time. This function is only called in editor mode. /// protected void Reset() { m_HasMigratedActions = true; m_PositionInput = new InputActionProperty(new InputAction("Position", expectedControlType: "Vector3")); m_RotationInput = new InputActionProperty(new InputAction("Rotation", expectedControlType: "Quaternion")); m_TrackingStateInput = new InputActionProperty(new InputAction("Tracking State", expectedControlType: "Integer")); } /// /// This function is called when the script instance is being loaded. /// protected virtual void Awake() { #if UNITY_INPUT_SYSTEM_ENABLE_VR && ENABLE_VR if (HasStereoCamera(out var cameraComponent)) { UnityEngine.XR.XRDevice.DisableAutoXRCameraTracking(cameraComponent, true); } #endif } /// /// This function is called when the object becomes enabled and active. /// protected void OnEnable() { InputSystem.onAfterUpdate += UpdateCallback; BindActions(); // Read current input values when becoming enabled, // but wait until after the input update so the input is read at a consistent time m_IsFirstUpdate = true; } /// /// This function is called when the object becomes disabled or inactive. /// protected void OnDisable() { UnbindActions(); InputSystem.onAfterUpdate -= UpdateCallback; } /// /// This function is called when the will be destroyed. /// protected virtual void OnDestroy() { #if UNITY_INPUT_SYSTEM_ENABLE_VR && ENABLE_VR if (HasStereoCamera(out var cameraComponent)) { UnityEngine.XR.XRDevice.DisableAutoXRCameraTracking(cameraComponent, false); } #endif } /// /// The callback method called after the Input System has completed an update and processed all pending events. /// /// protected void UpdateCallback() { if (m_IsFirstUpdate) { // Update current input values if this is the first update since becoming enabled // since the performed callbacks may not have been executed if (m_PositionInput.action != null) m_CurrentPosition = m_PositionInput.action.ReadValue(); if (m_RotationInput.action != null) m_CurrentRotation = m_RotationInput.action.ReadValue(); ReadTrackingState(); m_IsFirstUpdate = false; } if (InputState.currentUpdateType == InputUpdateType.BeforeRender) OnBeforeRender(); else OnUpdate(); } void ReadTrackingState() { var trackingStateAction = m_TrackingStateInput.action; if (trackingStateAction != null && !trackingStateAction.enabled) { // Treat a disabled action as the default None value for the ReadValue call m_CurrentTrackingState = TrackingStates.None; return; } if (trackingStateAction == null || trackingStateAction.m_BindingsCount == 0) { // Treat an Input Action Reference with no reference the same as // an enabled Input Action with no authored bindings, and allow driving the Transform pose. m_CurrentTrackingState = TrackingStates.Position | TrackingStates.Rotation; return; } // Grab state. var actionMap = trackingStateAction.GetOrCreateActionMap(); actionMap.ResolveBindingsIfNecessary(); var state = actionMap.m_State; // Get list of resolved controls to determine if a device actually has tracking state. var hasResolvedControl = false; if (state != null) { var actionIndex = trackingStateAction.m_ActionIndexInState; var totalBindingCount = state.totalBindingCount; for (var i = 0; i < totalBindingCount; ++i) { unsafe { ref var bindingState = ref state.bindingStates[i]; if (bindingState.actionIndex != actionIndex) continue; if (bindingState.isComposite) continue; if (bindingState.controlCount > 0) { hasResolvedControl = true; break; } } } } // Retain the current value if there is no resolved binding. // Since the field initializes to allowing position and rotation, // this allows for driving the Transform pose always when the device // doesn't support reporting the tracking state. if (hasResolvedControl) m_CurrentTrackingState = (TrackingStates)trackingStateAction.ReadValue(); } /// /// This method is called after the Input System has completed an update and processed all pending events /// when the type of update is not . /// protected virtual void OnUpdate() { if (m_UpdateType == UpdateType.Update || m_UpdateType == UpdateType.UpdateAndBeforeRender) { PerformUpdate(); } } /// /// This method is called after the Input System has completed an update and processed all pending events /// when the type of update is . /// protected virtual void OnBeforeRender() { if (m_UpdateType == UpdateType.BeforeRender || m_UpdateType == UpdateType.UpdateAndBeforeRender) { PerformUpdate(); } } /// /// Updates properties with the current input pose values that have been read, /// constrained by tracking type and tracking state. /// /// protected virtual void PerformUpdate() { SetLocalTransform(m_CurrentPosition, m_CurrentRotation); } /// /// Updates properties, constrained by tracking type and tracking state. /// /// The new local position to possibly set. /// The new local rotation to possibly set. protected virtual void SetLocalTransform(Vector3 newPosition, Quaternion newRotation) { var positionValid = m_IgnoreTrackingState || (m_CurrentTrackingState & TrackingStates.Position) != 0; var rotationValid = m_IgnoreTrackingState || (m_CurrentTrackingState & TrackingStates.Rotation) != 0; if (rotationValid && (m_TrackingType == TrackingType.RotationAndPosition || m_TrackingType == TrackingType.RotationOnly)) { transform.localRotation = newRotation; } if (positionValid && (m_TrackingType == TrackingType.RotationAndPosition || m_TrackingType == TrackingType.PositionOnly)) { transform.localPosition = newPosition; } } bool HasStereoCamera(out Camera cameraComponent) { return TryGetComponent(out cameraComponent) && cameraComponent.stereoEnabled; } #region DEPRECATED // Disable warnings that these fields are never assigned to. They are set during Unity deserialization and migrated. // ReSharper disable UnassignedField.Local #pragma warning disable 0649 [Obsolete] [SerializeField, HideInInspector] InputAction m_PositionAction; /// /// (Deprecated) The action to read the position value of a tracked device. /// Must support reading a value of type . /// /// public InputAction positionAction { get => m_PositionInput.action; set => positionInput = new InputActionProperty(value); } [Obsolete] [SerializeField, HideInInspector] InputAction m_RotationAction; /// /// (Deprecated) The action to read the rotation value of a tracked device. /// Must support reading a value of type . /// /// public InputAction rotationAction { get => m_RotationInput.action; set => rotationInput = new InputActionProperty(value); } #pragma warning restore 0649 // ReSharper restore UnassignedField.Local /// /// Stores whether the fields of type have been migrated to fields of type . /// [SerializeField, HideInInspector] bool m_HasMigratedActions; /// void ISerializationCallbackReceiver.OnBeforeSerialize() { } /// void ISerializationCallbackReceiver.OnAfterDeserialize() { if (m_HasMigratedActions) return; #pragma warning disable 0612 m_PositionInput = new InputActionProperty(m_PositionAction); m_RotationInput = new InputActionProperty(m_RotationAction); m_HasMigratedActions = true; #pragma warning restore 0612 } #endregion } }