Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
3 changes: 3 additions & 0 deletions Packages/com.unity.inputsystem/Documentation~/Events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 UIToolkit `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.
Comment thread
MorganHoarau marked this conversation as resolved.
Outdated

You can also write event traces out to files/streams, load them back in, and replay recorded streams.

```CSharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,8 @@ public class ReplayController : IDisposable
private double m_StartTimeAsPerRuntime;
private int m_AllEventsByTimeIndex = 0;
private List<InputEventPtr> m_AllEventsByTime;
private bool m_ReplayBypassActive;
private Action m_ClearReplayBypassCallback;

internal ReplayController(InputEventTrace trace)
{
Expand All @@ -1088,12 +1090,52 @@ public void Dispose()
{
InputSystem.onBeforeUpdate -= OnBeginFrame;
finished = true;
EndReplayBypass();

foreach (var device in m_CreatedDevices)
InputSystem.RemoveDevice(device);
m_CreatedDevices = default;
}

// 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_ReplayBypassActive)
{
m_ReplayBypassActive = true;
++InputSystem.s_Manager.m_ActiveReplayCount;
Comment thread
MorganHoarau marked this conversation as resolved.
Comment thread
MorganHoarau marked this conversation as resolved.
}
}

// 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)
m_ClearReplayBypassCallback = EndReplayBypass;
InputSystem.onAfterUpdate += m_ClearReplayBypassCallback;
Comment thread
MorganHoarau marked this conversation as resolved.
}

private void EndReplayBypass()
{
if (m_ClearReplayBypassCallback != null)
{
InputSystem.onAfterUpdate -= m_ClearReplayBypassCallback;
m_ClearReplayBypassCallback = null;
}
if (m_ReplayBypassActive)
{
m_ReplayBypassActive = false;
--InputSystem.s_Manager.m_ActiveReplayCount;
Comment thread
MorganHoarau marked this conversation as resolved.
Outdated
}
}

/// <summary>
/// Replay events recorded from <paramref name="recordedDevice"/> on device <paramref name="playbackDevice"/>.
/// </summary>
Expand Down Expand Up @@ -1249,6 +1291,7 @@ public ReplayController Rewind()
public ReplayController PlayAllFramesOneByOne()
{
finished = false;
BeginReplayBypass();
InputSystem.onBeforeUpdate += OnBeginFrame;
return this;
}
Expand All @@ -1267,6 +1310,7 @@ public ReplayController PlayAllFramesOneByOne()
public ReplayController PlayAllEvents()
{
finished = false;
BeginReplayBypass();
try
{
while (MoveNext(true, out var eventPtr))
Expand Down Expand Up @@ -1311,6 +1355,7 @@ public ReplayController PlayAllEventsAccordingToTimestamps()

// Start playback.
finished = false;
BeginReplayBypass();
m_StartTimeAsPerFirstEvent = -1;
m_AllEventsByTimeIndex = -1;
InputSystem.onBeforeUpdate += OnBeginFrame;
Expand Down Expand Up @@ -1381,6 +1426,9 @@ private void Finished()
{
finished = true;
InputSystem.onBeforeUpdate -= OnBeginFrame;
// 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();
m_OnFinished?.Invoke();
}

Expand Down
19 changes: 14 additions & 5 deletions Packages/com.unity.inputsystem/InputSystem/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,18 @@ public bool runPlayerUpdatesInEditMode
set => m_RunPlayerUpdatesInEditMode = value;
}

/// <summary>
/// Number of active <see cref="InputEventTrace.ReplayController"/> 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.
/// </summary>
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;
Expand All @@ -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
Expand Down Expand Up @@ -3371,15 +3381,15 @@ 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.
if (device != null && !device.enabled &&
// 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 && !isReplayActive &&
Comment thread
MorganHoarau marked this conversation as resolved.
Outdated
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
Expand Down Expand Up @@ -3410,7 +3420,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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
;
}

/// <summary>
Expand Down
Loading