diff --git a/Assets/Tests/InputSystem/Plugins/XRTests.cs b/Assets/Tests/InputSystem/Plugins/XRTests.cs index 65bc96c166..d222dd26b2 100644 --- a/Assets/Tests/InputSystem/Plugins/XRTests.cs +++ b/Assets/Tests/InputSystem/Plugins/XRTests.cs @@ -728,6 +728,40 @@ public void Components_TrackedPoseDriver_RetainsPoseWhenTrackedDeviceRemoved() } } + [Test] + [Category("Components")] + [TestCase(false)] + [TestCase(true)] + public void Components_TrackedPoseDriver_RetainsPoseWhenNoActionIsBound(bool ignoreTrackingState) + { + // Tests/reproduces the scenario described in https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-699 + // i.e. that rotation and/or position is not updated if device is not connected. + + var position = new Vector3(1f, 2f, 3f); + var rotation = new Quaternion(0.09853293f, 0.09853293f, 0.09853293f, 0.9853293f); + + // Setup GameObject to have a position and rotation that is different from identity transform + var go = new GameObject(); + go.transform.position = position; + go.transform.rotation = rotation; + + // Configure TrackedPoseDriver + var tpd = go.AddComponent(); + tpd.updateType = TrackedPoseDriver.UpdateType.Update; + tpd.trackingType = TrackedPoseDriver.TrackingType.RotationAndPosition; + tpd.ignoreTrackingState = ignoreTrackingState; + + var transform = tpd.transform; + Assert.That(transform.position, Is.EqualTo(position)); + Assert.That(transform.rotation, Is.EqualTo(rotation)); + + // Ensure that position and/or rotation is not affected by update. + InputSystem.Update(InputUpdateType.Dynamic); + + Assert.That(transform.position, Is.EqualTo(position)); + Assert.That(transform.rotation, Is.EqualTo(rotation)); + } + [Test] [Category("Layouts")] public void Layouts_PoseControlsCanBeCreatedBySubcontrols() @@ -739,7 +773,7 @@ public void Layouts_PoseControlsCanBeCreatedBySubcontrols() var generatedLayout = InputSystem.LoadLayout("XRInputV1::XRManufacturer::XRDevice"); Assert.That(generatedLayout, Is.Not.Null); - // A Pose control parent was created based off subcontrols + // A Pose control parent was created based off sub-controls var pose = generatedLayout["PoseControl"]; Assert.That(pose.layout, Is.EqualTo(new InternedString("Pose"))); } diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 7a33d17dfe..d34b2e1edc 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -24,6 +24,7 @@ however, it has to be formatted properly to pass verification tests. - Fixed arrow key navigation of Input Actions after Action rename. [ISXB-1024](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1024) - Fixed gamepad navigation in UI Toolkit TextField when using InputSystemUIInputModule. [UUM-77364](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-77364) - Fixed issue where asset editor window splitter positions were not persisted [ISXB-1316] +- Fixed a bug that would cause `TrackedPoseDriver` to update position and rotation when no HMD device is connected [ISXB-699](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-699) instead of keeping it unchanged. ### Changed - Changed default input action asset name from New Controls to New Actions. diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/XR/TrackedPoseDriver.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/XR/TrackedPoseDriver.cs index 59b92283b5..53e7d53a95 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/XR/TrackedPoseDriver.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/XR/TrackedPoseDriver.cs @@ -1,5 +1,6 @@ using System; using UnityEngine.InputSystem.LowLevel; +using UnityEngine.UIElements; namespace UnityEngine.InputSystem.XR { @@ -307,7 +308,7 @@ void BindTrackingState() } } - private void RenameAndEnable(InputAction action, string name) + private static void RenameAndEnable(InputAction action, string name) { #if UNITY_EDITOR Editor.InputExitPlayModeAnalytic.suppress = true; @@ -468,14 +469,21 @@ 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) + // since the performed callbacks may not have been executed. In case there is no bound control + // we preserve current transform by extracting transform values as initial values instead. + var hasResolvedPositionInputControl = HasResolvedControl(m_PositionInput.action); + if (hasResolvedPositionInputControl) m_CurrentPosition = m_PositionInput.action.ReadValue(); + else + m_CurrentPosition = transform.localPosition; - if (m_RotationInput.action != null) + var hasResolvedRotationInputControl = HasResolvedControl(m_RotationInput.action); + if (hasResolvedRotationInputControl) m_CurrentRotation = m_RotationInput.action.ReadValue(); + else + m_CurrentRotation = transform.localRotation; - ReadTrackingState(); + ReadTrackingState(hasResolvedPositionInputControl, hasResolvedRotationInputControl); m_IsFirstUpdate = false; } @@ -486,7 +494,7 @@ protected void UpdateCallback() OnUpdate(); } - void ReadTrackingState() + void ReadTrackingState(bool hasResolvedPositionInputControl, bool hasResolvedRotationInputControl) { var trackingStateAction = m_TrackingStateInput.action; if (trackingStateAction != null && !trackingStateAction.enabled) @@ -500,46 +508,24 @@ void ReadTrackingState() { // 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; + // Check if we have transform and rotation controls to drive the pose. + if (hasResolvedPositionInputControl && hasResolvedRotationInputControl) + m_CurrentTrackingState = TrackingStates.Position | TrackingStates.Rotation; + else if (hasResolvedPositionInputControl) + m_CurrentTrackingState = TrackingStates.Position; + else if (hasResolvedRotationInputControl) + m_CurrentTrackingState = TrackingStates.Rotation; + else + m_CurrentTrackingState = TrackingStates.None; } - - // 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) + else if (HasResolvedControl(trackingStateAction)) { - 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) + // 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. m_CurrentTrackingState = (TrackingStates)trackingStateAction.ReadValue(); + } } /// @@ -585,6 +571,8 @@ protected virtual void PerformUpdate() /// The new local rotation to possibly set. protected virtual void SetLocalTransform(Vector3 newPosition, Quaternion newRotation) { + // Note that tracking state will be set to reflect whether the position and/or rotation + // actions can provide applicable values. var positionValid = m_IgnoreTrackingState || (m_CurrentTrackingState & TrackingStates.Position) != 0; var rotationValid = m_IgnoreTrackingState || (m_CurrentTrackingState & TrackingStates.Rotation) != 0; @@ -616,6 +604,42 @@ bool HasStereoCamera(out Camera cameraComponent) return TryGetComponent(out cameraComponent) && cameraComponent.stereoEnabled; } + // Evaluates whether the given action has at least one resolved control and may generate input. + private static bool HasResolvedControl(InputAction action) + { + // Action cannot have controls if null. + if (action == null) + return false; + + // Attempt to grab state and resolve bindings unless already resolved. + var actionMap = action.GetOrCreateActionMap(); + actionMap.ResolveBindingsIfNecessary(); + var state = actionMap.m_State; + if (state == null) + return false; + + // Get list of resolved controls to determine if a device actually has a tracking state. + var actionIndex = action.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) + return true; + } + } + + return false; + } + #region DEPRECATED // Disable warnings that these fields are never assigned to. They are set during Unity deserialization and migrated.