diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index a2ece4afba..93ab93a083 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed +- Fixed `InputRecorder` playback not starting when the Game View is not focused in the Editor [ISXB-1319](https://jira.unity3d.com/browse/ISXB-1319) - Fixed a `NullReferenceException` thrown when removing all action maps [UUM-137116](https://jira.unity3d.com/browse/UUM-137116) - Simplified default setting messaging by consolidating repetitive messages into a single HelpBox. - Fixed a `NullPointerReferenceException` thrown in `InputManagerStateMonitors.FireStateChangeNotifications` logging by adding validation [UUM-136095]. diff --git a/Packages/com.unity.inputsystem/Documentation~/Events.md b/Packages/com.unity.inputsystem/Documentation~/Events.md index f93ce87ba1..a5fb7ecbc5 100644 --- a/Packages/com.unity.inputsystem/Documentation~/Events.md +++ b/Packages/com.unity.inputsystem/Documentation~/Events.md @@ -223,6 +223,9 @@ trace.Dispose(); Dispose event traces after use, so that they do not leak memory on the unmanaged (C++) memory heap. +> [!NOTE] +> **Keyboard text input is not replayed to UI text fields.** Keyboard state (key presses) is captured and replayed correctly and remains accessible via `Keyboard.current`. However, there is a known limitation with character delivery to UI Framework components (uGUI `InputField` or UI Toolkit `TextField`). These components receive text through a separate native pipeline that is not fed by event replay. As a result, text typed into UI text fields during recording will not appear during playback. + You can also write event traces out to files/streams, load them back in, and replay recorded streams. ```CSharp diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs index c8b3a40413..8d7c1dd688 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Events/InputEventTrace.cs @@ -1072,6 +1072,9 @@ public class ReplayController : IDisposable private double m_StartTimeAsPerRuntime; private int m_AllEventsByTimeIndex = 0; private List m_AllEventsByTime; +#if UNITY_EDITOR + private bool m_EditorEventPassthroughActive; +#endif internal ReplayController(InputEventTrace trace) { @@ -1088,12 +1091,52 @@ public void Dispose() { InputSystem.onBeforeUpdate -= OnBeginFrame; finished = true; - +#if UNITY_EDITOR + StopEditorEventPassthrough(); +#endif foreach (var device in m_CreatedDevices) InputSystem.RemoveDevice(device); m_CreatedDevices = default; } +#if UNITY_EDITOR + private void StartEditorEventPassthrough() + { + if (!m_EditorEventPassthroughActive) + { + m_EditorEventPassthroughActive = true; + InputManager.StartEditorEventPassthrough(); + } + } + + private void StopEditorEventPassthrough() + { + // Clean up any pending deferred stop. + InputSystem.onAfterUpdate -= DeferredStopEditorEventPassthrough; + + if (m_EditorEventPassthroughActive) + { + m_EditorEventPassthroughActive = false; + InputManager.StopEditorEventPassthrough(); + } + } + + // Defers passthrough stop to after the current update so events already queued + // in the native buffer are still processed with passthrough active. + private void ScheduleStopEditorEventPassthrough() + { + if (!m_EditorEventPassthroughActive) + return; + InputSystem.onAfterUpdate += DeferredStopEditorEventPassthrough; + } + + private void DeferredStopEditorEventPassthrough() + { + InputSystem.onAfterUpdate -= DeferredStopEditorEventPassthrough; + StopEditorEventPassthrough(); + } + +#endif /// /// Replay events recorded from on device . /// @@ -1249,6 +1292,9 @@ public ReplayController Rewind() public ReplayController PlayAllFramesOneByOne() { finished = false; +#if UNITY_EDITOR + StartEditorEventPassthrough(); +#endif InputSystem.onBeforeUpdate += OnBeginFrame; return this; } @@ -1267,6 +1313,9 @@ public ReplayController PlayAllFramesOneByOne() public ReplayController PlayAllEvents() { finished = false; +#if UNITY_EDITOR + StartEditorEventPassthrough(); +#endif try { while (MoveNext(true, out var eventPtr)) @@ -1311,6 +1360,9 @@ public ReplayController PlayAllEventsAccordingToTimestamps() // Start playback. finished = false; +#if UNITY_EDITOR + StartEditorEventPassthrough(); +#endif m_StartTimeAsPerFirstEvent = -1; m_AllEventsByTimeIndex = -1; InputSystem.onBeforeUpdate += OnBeginFrame; @@ -1381,6 +1433,11 @@ private void Finished() { finished = true; InputSystem.onBeforeUpdate -= OnBeginFrame; +#if UNITY_EDITOR + // Defer passthrough stop so events already queued in the native buffer + // this frame are still processed with passthrough active. + ScheduleStopEditorEventPassthrough(); +#endif m_OnFinished?.Invoke(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs index 4be52778df..431479d431 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputManager.cs @@ -492,8 +492,45 @@ public bool runPlayerUpdatesInEditMode set => m_RunPlayerUpdatesInEditMode = value; } + /// + /// Ref-counted flag that bypasses Game View focus gating for event processing. + /// When greater than zero, events are processed as if the Game View has focus, + /// regardless of actual focus state. This affects event routing, disabled-device + /// discard, and UI module processing. + /// + /// + /// Use / + /// to manage this counter. Follows the same pattern as + /// AssetDatabase.StartAssetEditing/StopAssetEditing. + /// + /// + /// + private int m_EditorEventPassthroughCount; + + internal bool isEditorEventPassthroughActive => m_EditorEventPassthroughCount > 0; + + /// + /// Signals that events should bypass Game View focus gating. Ref-counted: + /// each call must be balanced by a corresponding . + /// + internal static void StartEditorEventPassthrough() + { + ++InputSystem.s_Manager.m_EditorEventPassthroughCount; + } + + /// + /// Signals that the caller no longer needs events to bypass Game View focus gating. + /// Decrements the ref count started by . + /// + internal static void StopEditorEventPassthrough() + { + if (InputSystem.s_Manager != null && InputSystem.s_Manager.m_EditorEventPassthroughCount > 0) + --InputSystem.s_Manager.m_EditorEventPassthroughCount; + } + #endif // UNITY_EDITOR + private bool gameIsPlaying => #if UNITY_EDITOR (m_Runtime.isInPlayMode && !m_Runtime.isEditorPaused) || m_RunPlayerUpdatesInEditMode; @@ -504,7 +541,7 @@ public bool runPlayerUpdatesInEditMode private bool gameHasFocus => #if UNITY_EDITOR - m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus; + m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus || isEditorEventPassthroughActive; #else applicationHasFocus || gameShouldGetInputRegardlessOfFocus; #endif @@ -3372,7 +3409,12 @@ private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEven // If device is disabled, we let the event through only in certain cases. // Removal and configuration change events should always be processed. + // During replay, allow events through for devices disabled due to background + // focus loss — the replay intentionally re-injects events for those devices. if (device != null && !device.enabled && +#if UNITY_EDITOR + !isEditorEventPassthroughActive && +#endif currentEventType != DeviceRemoveEvent.Type && currentEventType != DeviceConfigurationEvent.Type && (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime | @@ -3411,7 +3453,6 @@ private unsafe void ProcessEventBuffer(InputUpdateType updateType, ref InputEven #endif if (!shouldProcess) { - // Skip event if PreProcessEvent considers it to be irrelevant. m_InputEventStream.Advance(false); continue; } diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs index 0108a76cec..bcd42b9332 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/UI/InputSystemUIInputModule.cs @@ -997,7 +997,11 @@ private bool shouldIgnoreFocus // if running in the background is enabled, we already have rules in place what kind of input // is allowed through and what isn't. And for the input that *IS* allowed through, the UI should // react. - get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground; + get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground +#if UNITY_EDITOR + || InputSystem.s_Manager.isEditorEventPassthroughActive +#endif + ; } ///