-
Notifications
You must be signed in to change notification settings - Fork 331
FIX: handled events still triggering actions when switching PlayerInput #2340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
5a164b3
1fd3ec5
c950b4f
75f5066
ee9aace
94564dc
2f1d804
8c58fe7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| using UnityEngine; | ||
| using UnityEngine.Scripting; | ||
| using UnityEngine.InputSystem; | ||
| using UnityEngine.InputSystem.Users; | ||
| using UnityEngine.InputSystem.Controls; | ||
| using UnityEngine.InputSystem.Layouts; | ||
| using UnityEngine.InputSystem.LowLevel; | ||
|
|
@@ -1256,6 +1257,48 @@ public void Events_CanPreventEventsFromBeingProcessed() | |
| Assert.That(device.rightTrigger.ReadValue(), Is.EqualTo(0.0).Within(0.00001)); | ||
| } | ||
|
|
||
| [Test] | ||
| [Category("Events")] | ||
| public void Events_HandledFirstEvent_DoesNotTriggerAction_OnSecondEventSameUpdate() | ||
| { | ||
| var gamepad = InputSystem.AddDevice<Gamepad>(); | ||
| var action = new InputAction(type: InputActionType.Button, binding: "<Gamepad>/buttonSouth"); | ||
| action.Enable(); | ||
|
|
||
| var performedCount = 0; | ||
| action.performed += _ => ++performedCount; | ||
|
|
||
| var previousPolicy = InputSystem.s_Manager.inputEventHandledPolicy; | ||
| InputSystem.s_Manager.inputEventHandledPolicy = InputEventHandledPolicy.SuppressStateUpdates; | ||
|
|
||
| InputUser.listenForUnpairedDeviceActivity++; | ||
| var handledFirstEventId = 0; | ||
| Action<InputControl, InputEventPtr> onUnpaired = (control, eventPtr) => | ||
| { | ||
| if (handledFirstEventId == 0) | ||
| { | ||
| handledFirstEventId = eventPtr.id; | ||
| eventPtr.handled = true; | ||
| } | ||
| }; | ||
| InputUser.onUnpairedDeviceUsed += onUnpaired; | ||
|
|
||
| try | ||
| { | ||
| InputSystem.QueueStateEvent(gamepad, new GamepadState().WithButton(GamepadButton.South)); | ||
| InputSystem.QueueStateEvent(gamepad, new GamepadState().WithButton(GamepadButton.South)); | ||
| InputSystem.Update(); | ||
|
|
||
| Assert.That(performedCount, Is.EqualTo(0)); | ||
| } | ||
| finally | ||
| { | ||
| InputUser.onUnpairedDeviceUsed -= onUnpaired; | ||
| InputUser.listenForUnpairedDeviceActivity--; | ||
| InputSystem.s_Manager.inputEventHandledPolicy = previousPolicy; | ||
| } | ||
| } | ||
|
|
||
| [Test] | ||
| [Category("Events")] | ||
| public void EventHandledPolicy_ShouldReflectUserSetting() | ||
|
|
@@ -1292,24 +1335,24 @@ class SuppressedActionEventData | |
| // Step 3: Release button north and stick while no longer being suppressed. | ||
| // Step 4: Press gamepad north and stick. | ||
|
|
||
| // Press event is detected in step 2 (false positive) with default interaction | ||
| // Press event is suppressed in step 1/2 with default interaction | ||
| [TestCase(InputEventHandledPolicy.SuppressStateUpdates, // policy | ||
| null, // interactions | ||
| new int[] { 0, 0, 1, 1, 2}, // started | ||
| new int[] { 0, 0, 1, 1, 2}, // performed | ||
| new int[] {0, 0, 0, 1, 1})] // cancelled | ||
| new int[] { 0, 0, 0, 0, 1}, // started | ||
| new int[] { 0, 0, 0, 0, 1}, // performed | ||
| new int[] {0, 0, 0, 0, 0})] // cancelled | ||
| // Press event is not detected in step 1/2 with default interaction | ||
| [TestCase(InputEventHandledPolicy.SuppressActionEventNotifications, | ||
| null, | ||
| new int[] { 0, 0, 0, 0, 1}, | ||
| new int[] { 0, 0, 0, 0, 1}, | ||
| new int[] {0, 0, 0, 1, 1})] | ||
| // Press event is detected in step 2 (false positive) with explicit press interaction | ||
| // Press event is suppressed in step 1/2 with explicit press interaction | ||
| [TestCase(InputEventHandledPolicy.SuppressStateUpdates, | ||
| "press", | ||
| new int[] { 0, 0, 1, 1, 2}, | ||
| new int[] { 0, 0, 1, 1, 2}, | ||
| new int[] {0, 0, 0, 1, 1})] | ||
| new int[] { 0, 0, 0, 0, 1}, | ||
| new int[] { 0, 0, 0, 0, 1}, | ||
| new int[] {0, 0, 0, 0, 0})] | ||
|
Comment on lines
+1341
to
+1355
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a behavior change. What is the reason to change this?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We needed a explicit way to keep action suppression working across update steps. It will ensure that actions doesn't trigger on next update which shows up as a false positive press like reported in the bug. Please correct me if I'm wrong - my understanding is,
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. I'll keep looking at it to see if I find any problems. Meanwhile maybe @ekcoh has a more immediate answer since he added these tests. |
||
| // Press event is not detected in step 1/2 (false positive) with explicit press interaction | ||
| [TestCase(InputEventHandledPolicy.SuppressActionEventNotifications, | ||
| "press", | ||
|
|
@@ -1396,7 +1439,7 @@ public void Events_ShouldRespectHandledPolicyUponUpdateAndSuppressedPressTransit | |
| Assert.That(action.WasPressedThisFrame, Is.EqualTo(performedThisFrame)); | ||
| releasedThisFrame = expectedCancelled[2] - expectedCancelled[1] > 0; | ||
| Assert.That(action.WasReleasedThisFrame, Is.EqualTo(releasedThisFrame)); | ||
| Assert.That(action.IsPressed, Is.True); // Note: This is not an event and hence not suppressed | ||
| Assert.That(action.IsPressed, Is.EqualTo(seesControlChangesUnderSuppression)); // Note: This is not an event and hence not suppressed | ||
|
|
||
| Assert.That(Gamepad.current.buttonNorth.wasPressedThisFrame, Is.EqualTo(!seesControlChangesUnderSuppression)); | ||
| Assert.That(Gamepad.current.buttonNorth.wasReleasedThisFrame, Is.False); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2236,6 +2236,7 @@ | |
| internal CallbackArray<InputDeviceCommandDelegate> m_DeviceCommandCallbacks; | ||
| private CallbackArray<LayoutChangeListener> m_LayoutChangeListeners; | ||
| private CallbackArray<EventListener> m_EventListeners; | ||
| private uint[] m_SuppressActionsForDeviceUpdate; | ||
| private CallbackArray<UpdateListener> m_BeforeUpdateListeners; | ||
| private CallbackArray<UpdateListener> m_AfterUpdateListeners; | ||
| private CallbackArray<Action> m_SettingsChangedListeners; | ||
|
|
@@ -3454,22 +3455,22 @@ | |
| } | ||
| } | ||
|
|
||
| var currentEventPtr = new InputEventPtr(currentEventReadPtr); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we need to cache this explicitly or if doing it the previous way is enough, by using |
||
|
|
||
| // Give listeners a shot at the event. | ||
| // NOTE: We call listeners also for events where the device is disabled. This is crucial for code | ||
| // such as TouchSimulation that disables the originating devices and then uses its events to | ||
| // create simulated events from. | ||
| if (m_EventListeners.length > 0) | ||
| { | ||
| DelegateHelpers.InvokeCallbacksSafe(ref m_EventListeners, | ||
| new InputEventPtr(currentEventReadPtr), device, k_InputOnEventMarker, "InputSystem.onEvent"); | ||
| DelegateHelpers.InvokeCallbacksSafe(ref m_EventListeners, currentEventPtr, device, k_InputOnEventMarker, "InputSystem.onEvent"); | ||
| } | ||
|
|
||
| // If a listener marks the event as handled, we don't process it further. | ||
| if (m_InputEventHandledPolicy == InputEventHandledPolicy.SuppressStateUpdates && | ||
| currentEventReadPtr->handled) | ||
| { | ||
| m_InputEventStream.Advance(false); | ||
| continue; | ||
| } | ||
| if (m_InputEventHandledPolicy == InputEventHandledPolicy.SuppressStateUpdates && currentEventPtr.handled) | ||
| { | ||
| SuppressActionsForDevice(device); | ||
| m_InputEventStream.Advance(false); | ||
| continue; | ||
| } | ||
|
|
||
| // Update metrics. | ||
|
|
@@ -3484,8 +3485,6 @@ | |
| case StateEvent.Type: | ||
| case DeltaStateEvent.Type: | ||
|
|
||
| var eventPtr = new InputEventPtr(currentEventReadPtr); | ||
|
|
||
| // Ignore the event if the last state update we received for the device was | ||
| // newer than this state event is. We don't allow devices to go back in time. | ||
| // | ||
|
|
@@ -3496,7 +3495,7 @@ | |
| // increasing timestamps across all such streams. | ||
| var deviceIsStateCallbackReceiver = device.hasStateCallbacks; | ||
| if (currentEventTimeInternal < device.m_LastUpdateTimeInternal && | ||
| !(deviceIsStateCallbackReceiver && device.stateBlock.format != eventPtr.stateFormat)) | ||
| !(deviceIsStateCallbackReceiver && device.stateBlock.format != currentEventPtr.stateFormat)) | ||
| { | ||
| #if UNITY_EDITOR | ||
| m_Diagnostics?.OnEventTimestampOutdated(new InputEventPtr(currentEventReadPtr), device); | ||
|
|
@@ -3520,38 +3519,38 @@ | |
| m_ShouldMakeCurrentlyUpdatingDeviceCurrent = true; | ||
| // NOTE: We leave it to the device to make sure the event has the right format. This allows the | ||
| // device to handle multiple different incoming formats. | ||
| ((IInputStateCallbackReceiver)device).OnStateEvent(eventPtr); | ||
| ((IInputStateCallbackReceiver)device).OnStateEvent(currentEventPtr); | ||
|
|
||
| haveChangedStateOtherThanNoise = m_ShouldMakeCurrentlyUpdatingDeviceCurrent; | ||
| } | ||
| else | ||
| { | ||
| // If the state format doesn't match, ignore the event. | ||
| if (device.stateBlock.format != eventPtr.stateFormat) | ||
| if (device.stateBlock.format != currentEventPtr.stateFormat) | ||
| { | ||
| #if UNITY_EDITOR | ||
| m_Diagnostics?.OnEventFormatMismatch(currentEventReadPtr, device); | ||
| #endif | ||
| break; | ||
| } | ||
|
|
||
| haveChangedStateOtherThanNoise = UpdateState(device, eventPtr, updateType); | ||
| haveChangedStateOtherThanNoise = UpdateState(device, currentEventPtr, updateType); | ||
| } | ||
|
|
||
| totalEventBytesProcessed += eventPtr.sizeInBytes; | ||
| totalEventBytesProcessed += currentEventPtr.sizeInBytes; | ||
|
|
||
| device.m_CurrentProcessedEventBytesOnUpdate += eventPtr.sizeInBytes; | ||
| device.m_CurrentProcessedEventBytesOnUpdate += currentEventPtr.sizeInBytes; | ||
|
|
||
| // Update timestamp on device. | ||
| // NOTE: We do this here and not in UpdateState() so that InputState.Change() will *NOT* change timestamps. | ||
| // Only events should. If running play mode updates in editor, we want to defer to the play mode | ||
| // callbacks to set the last update time to avoid dropping events only processed by the editor state. | ||
| if (device.m_LastUpdateTimeInternal <= eventPtr.internalTime | ||
| if (device.m_LastUpdateTimeInternal <= currentEventPtr.internalTime | ||
| #if UNITY_EDITOR | ||
| && !(updateType == InputUpdateType.Editor && runPlayerUpdatesInEditMode) | ||
| #endif | ||
| ) | ||
| device.m_LastUpdateTimeInternal = eventPtr.internalTime; | ||
| device.m_LastUpdateTimeInternal = currentEventPtr.internalTime; | ||
|
|
||
| // Make device current. Again, only do this when receiving events. | ||
| if (haveChangedStateOtherThanNoise) | ||
|
|
@@ -3890,6 +3889,35 @@ | |
|
|
||
| private bool m_ShouldMakeCurrentlyUpdatingDeviceCurrent; | ||
|
|
||
| // Record the update step for a handled event so we can suppress actions for this device | ||
| // in this update and the immediately following one (action processing may occur next step). | ||
| private void SuppressActionsForDevice(InputDevice device) | ||
| { | ||
| if (m_InputEventHandledPolicy != InputEventHandledPolicy.SuppressStateUpdates || device == null) | ||
| return; | ||
|
|
||
| var deviceIndex = device.m_DeviceIndex; | ||
| if (m_SuppressActionsForDeviceUpdate == null || deviceIndex >= m_SuppressActionsForDeviceUpdate.Length) | ||
| Array.Resize(ref m_SuppressActionsForDeviceUpdate, deviceIndex + 1); | ||
| m_SuppressActionsForDeviceUpdate[deviceIndex] = InputUpdate.s_UpdateStepCount; | ||
| } | ||
|
|
||
| // Check whether actions for this device should be suppressed due to a handled event | ||
| // in the current or immediately previous update step. | ||
| internal bool ShouldSuppressActionsForDevice(InputDevice device) | ||
| { | ||
| if (m_InputEventHandledPolicy != InputEventHandledPolicy.SuppressStateUpdates || device == null) | ||
| return false; | ||
|
|
||
| var deviceIndex = device.m_DeviceIndex; | ||
| if (m_SuppressActionsForDeviceUpdate == null || deviceIndex >= m_SuppressActionsForDeviceUpdate.Length) | ||
| return false; | ||
|
|
||
| var lastUpdate = (int)m_SuppressActionsForDeviceUpdate[deviceIndex]; | ||
| var currentUpdate = (int)InputUpdate.s_UpdateStepCount; | ||
| return lastUpdate == currentUpdate || lastUpdate + 1 == currentUpdate; | ||
| } | ||
AswinRajGopal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // This is a dirty hot fix to expose entropy from device back to input manager to make a choice if we want to make device current or not. | ||
| // A proper fix would be to change IInputStateCallbackReceiver.OnStateEvent to return bool to make device current or not. | ||
| internal void DontMakeCurrentlyUpdatingDeviceCurrent() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If any of this changes I suspect it will regress the rebinding sample.