diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 2813c56348..e8fbbac999 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/Events/InputEventTrace.cs b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs index c8b3a40413..108e811122 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Events/InputEventTrace.cs @@ -1072,6 +1072,10 @@ public class ReplayController : IDisposable private double m_StartTimeAsPerRuntime; private int m_AllEventsByTimeIndex = 0; private List m_AllEventsByTime; +#if UNITY_EDITOR + private bool m_ReplayBypassActive; + private Action m_ClearReplayBypassCallback; +#endif internal ReplayController(InputEventTrace trace) { @@ -1088,12 +1092,65 @@ public void Dispose() { InputSystem.onBeforeUpdate -= OnBeginFrame; finished = true; - +#if UNITY_EDITOR + EndReplayBypass(); +#endif foreach (var device in m_CreatedDevices) InputSystem.RemoveDevice(device); m_CreatedDevices = default; } +#if UNITY_EDITOR + // Signals InputManager to treat events as if game view has focus, bypassing + // editor focus routing that would otherwise defer pointer/keyboard events to + // editor updates where they reach the editor UI instead of the game. + private void BeginReplayBypass() + { + if (m_ClearReplayBypassCallback != null) + { + InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback; + } + + if (!m_ReplayBypassActive) + { + m_ReplayBypassActive = true; + ++InputSystem.s_Manager.m_ActiveReplayCount; + } + } + + // Schedules the bypass to be cleared after the current OnUpdate finishes processing + // events (via onAfterUpdate). This ensures events already queued in the native buffer + // are still processed with the bypass active before it is removed. + private void ScheduleEndReplayBypass() + { + if (!m_ReplayBypassActive) + return; + + if (m_ClearReplayBypassCallback != null) + { + return; + } + + m_ClearReplayBypassCallback = EndReplayBypass; + InputSystem.onAfterUpdate += m_ClearReplayBypassCallback; + } + + private void EndReplayBypass() + { + if (m_ClearReplayBypassCallback != null) + { + InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback; + m_ClearReplayBypassCallback = null; + } + if (m_ReplayBypassActive) + { + m_ReplayBypassActive = false; + if (InputSystem.s_Manager != null) + --InputSystem.s_Manager.m_ActiveReplayCount; + } + } + +#endif /// /// Replay events recorded from on device . /// @@ -1249,6 +1306,9 @@ public ReplayController Rewind() public ReplayController PlayAllFramesOneByOne() { finished = false; +#if UNITY_EDITOR + BeginReplayBypass(); +#endif InputSystem.onBeforeUpdate += OnBeginFrame; return this; } @@ -1267,6 +1327,9 @@ public ReplayController PlayAllFramesOneByOne() public ReplayController PlayAllEvents() { finished = false; +#if UNITY_EDITOR + BeginReplayBypass(); +#endif try { while (MoveNext(true, out var eventPtr)) @@ -1311,6 +1374,9 @@ public ReplayController PlayAllEventsAccordingToTimestamps() // Start playback. finished = false; +#if UNITY_EDITOR + BeginReplayBypass(); +#endif m_StartTimeAsPerFirstEvent = -1; m_AllEventsByTimeIndex = -1; InputSystem.onBeforeUpdate += OnBeginFrame; @@ -1381,6 +1447,11 @@ private void Finished() { finished = true; InputSystem.onBeforeUpdate -= OnBeginFrame; +#if UNITY_EDITOR + // Schedule bypass removal for after the next OnUpdate, so any events already + // queued into the native buffer this frame are still processed with the bypass active. + ScheduleEndReplayBypass(); +#endif m_OnFinished?.Invoke(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index 128aca347c..94741bf73c 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -500,8 +500,18 @@ public bool runPlayerUpdatesInEditMode set => m_RunPlayerUpdatesInEditMode = value; } + /// + /// Number of active instances currently replaying. + /// When greater than zero, focus-based gating is bypassed so that replayed events reach the game + /// regardless of Game View focus. This affects event routing (A), disabled-device discard (B), + /// and UI module processing (C). See ISXB-1319. + /// + internal int m_ActiveReplayCount; + + internal bool isReplayActive => m_ActiveReplayCount > 0; #endif // UNITY_EDITOR + private bool gameIsPlaying => #if UNITY_EDITOR (m_Runtime.isInPlayMode && !UnityEditor.EditorApplication.isPaused) || m_RunPlayerUpdatesInEditMode; @@ -512,7 +522,7 @@ public bool runPlayerUpdatesInEditMode private bool gameHasFocus => #if UNITY_EDITOR - m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus; + m_RunPlayerUpdatesInEditMode || applicationHasFocus || gameShouldGetInputRegardlessOfFocus || isReplayActive; #else applicationHasFocus || gameShouldGetInputRegardlessOfFocus; #endif @@ -3371,15 +3381,18 @@ 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 + !isReplayActive && +#endif currentEventType != DeviceRemoveEvent.Type && currentEventType != DeviceConfigurationEvent.Type && (device.m_DeviceFlags & (InputDevice.DeviceFlags.DisabledInRuntime | InputDevice.DeviceFlags.DisabledWhileInBackground)) != 0) { #if UNITY_EDITOR - // If the device is disabled in the backend, getting events for them - // is something that indicates a problem in the backend so diagnose. if ((device.m_DeviceFlags & InputDevice.DeviceFlags.DisabledInRuntime) != 0) m_Diagnostics?.OnEventForDisabledDevice(currentEventReadPtr, device); #endif @@ -3410,7 +3423,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/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index adba24a25e..fbb9ff3fcb 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -1001,7 +1001,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.isReplayActive +#endif + ; } ///