Skip to content

Commit da6127d

Browse files
K-Toneclaude
andauthored
FIX: [Port] [UUM-150956] [Input System] Fixed PlayerInput Unity Events missing until reselected (#2503)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 54b1581 commit da6127d

5 files changed

Lines changed: 248 additions & 9 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
using System.Collections;
2+
using System.Linq;
3+
using NUnit.Framework;
4+
using UnityEditor;
5+
using UnityEngine;
6+
using UnityEngine.InputSystem;
7+
using UnityEngine.InputSystem.Editor;
8+
using UnityEngine.TestTools;
9+
using UnityEngine.UIElements;
10+
using Object = UnityEngine.Object;
11+
12+
/// <summary>
13+
/// Tests for the custom inspector of the PlayerInput component.
14+
/// </summary>
15+
internal sealed class PlayerInputEditorTests
16+
{
17+
private GameObject go;
18+
private PlayerInput playerInput;
19+
private InputActionAsset actions;
20+
private PlayerInputEditor editor;
21+
private EditorWindow window;
22+
private bool onGuiCalled;
23+
24+
private class TestHostWindow : EditorWindow {}
25+
26+
[SetUp]
27+
public void SetUp()
28+
{
29+
go = new GameObject("Host");
30+
playerInput = go.AddComponent<PlayerInput>();
31+
actions = AssetDatabase.LoadAssetAtPath<InputActionAsset>(PlayerInputEditor.kDefaultInputActionsAssetPath);
32+
window = EditorWindow.GetWindow<TestHostWindow>();
33+
}
34+
35+
[TearDown]
36+
public void TearDown()
37+
{
38+
if (editor != null)
39+
Object.DestroyImmediate(editor);
40+
window.Close();
41+
Undo.ClearUndo(playerInput);
42+
Object.DestroyImmediate(go);
43+
}
44+
45+
private IEnumerator RenderInspector()
46+
{
47+
onGuiCalled = false;
48+
49+
var container = new IMGUIContainer(() =>
50+
{
51+
// Like the Inspector window, pick up changes made to the target outside the editor (e.g. undo).
52+
editor.serializedObject.UpdateIfRequiredOrScript();
53+
editor.OnInspectorGUI();
54+
onGuiCalled = true;
55+
});
56+
57+
window.rootVisualElement.Add(container);
58+
window.Repaint();
59+
60+
var timeoutTime = Time.realtimeSinceStartupAsDouble + 3.0;
61+
do
62+
{
63+
yield return null; // let Unity process a frame
64+
}
65+
while (!onGuiCalled && Time.realtimeSinceStartupAsDouble < timeoutTime);
66+
67+
container.RemoveFromHierarchy();
68+
Assert.That(onGuiCalled, Is.True, "Inspector GUI was not drawn");
69+
}
70+
71+
// Replaces the actions of the asset with the given ones in a single map, the way a reimport does. The IDs are
72+
// derived from the position, so actions keep their ID across calls.
73+
private static void LoadActions(InputActionAsset asset, params string[] actionNames)
74+
{
75+
var actionsJson = actionNames.Select((name, i) =>
76+
$"{{\"name\":\"{name}\",\"type\":\"Button\",\"id\":\"00000000-0000-0000-0000-{i + 1:D12}\"}}");
77+
asset.LoadFromJson(
78+
$"{{\"maps\":[{{\"name\":\"map\",\"actions\":[{string.Join(",", actionsJson)}],\"bindings\":[]}}]}}");
79+
}
80+
81+
// UUM-150956: switching to InvokeUnityEvents on a freshly added component did not show the per-action events
82+
// until the inspector was recreated.
83+
[UnityTest]
84+
public IEnumerator ActionEventsAreListed_WhenBehaviorIsSwitchedToInvokeUnityEvents(
85+
[Values(PlayerNotifications.SendMessages, PlayerNotifications.BroadcastMessages, PlayerNotifications.InvokeCSharpEvents)]
86+
PlayerNotifications initialBehavior)
87+
{
88+
Assert.That(actions, Is.Not.Null);
89+
90+
playerInput.m_Actions = actions;
91+
playerInput.m_NotificationBehavior = initialBehavior;
92+
editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput);
93+
94+
yield return RenderInspector();
95+
Assert.That(playerInput.m_ActionEvents, Is.Null.Or.Empty);
96+
97+
editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior)).intValue =
98+
(int)PlayerNotifications.InvokeUnityEvents;
99+
editor.serializedObject.ApplyModifiedPropertiesWithoutUndo();
100+
101+
yield return RenderInspector();
102+
103+
Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId),
104+
Is.EquivalentTo(actions.Select(a => a.id.ToString())));
105+
}
106+
107+
[UnityTest]
108+
public IEnumerator ActionEventsFollowActionsAsset_WhenSwitchingBackToInvokeUnityEvents()
109+
{
110+
Assert.That(actions, Is.Not.Null);
111+
112+
playerInput.m_Actions = actions;
113+
playerInput.m_NotificationBehavior = PlayerNotifications.InvokeUnityEvents;
114+
editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput);
115+
116+
yield return RenderInspector();
117+
Assert.That(playerInput.m_ActionEvents, Is.Not.Empty);
118+
119+
// Switch away, swap in an asset with different actions, then switch back.
120+
var otherActions = ScriptableObject.CreateInstance<InputActionAsset>();
121+
try
122+
{
123+
otherActions.AddActionMap("map").AddAction("other");
124+
125+
var behaviorProperty = editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior));
126+
behaviorProperty.intValue = (int)PlayerNotifications.SendMessages;
127+
editor.serializedObject.ApplyModifiedPropertiesWithoutUndo();
128+
yield return RenderInspector();
129+
130+
editor.serializedObject.FindProperty(nameof(PlayerInput.m_Actions)).objectReferenceValue = otherActions;
131+
editor.serializedObject.ApplyModifiedPropertiesWithoutUndo();
132+
yield return RenderInspector();
133+
134+
behaviorProperty.intValue = (int)PlayerNotifications.InvokeUnityEvents;
135+
editor.serializedObject.ApplyModifiedPropertiesWithoutUndo();
136+
yield return RenderInspector();
137+
138+
Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId),
139+
Is.EquivalentTo(otherActions.Select(a => a.id.ToString())));
140+
}
141+
finally
142+
{
143+
Object.DestroyImmediate(otherActions);
144+
}
145+
}
146+
147+
// Leaving InvokeUnityEvents and coming back with the same asset must pick up actions added in the meantime.
148+
[UnityTest]
149+
public IEnumerator ActionEventsFollowActionsAsset_WhenReenteringInvokeUnityEventsWithSameAsset()
150+
{
151+
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
152+
try
153+
{
154+
LoadActions(asset, "first");
155+
156+
playerInput.m_Actions = asset;
157+
playerInput.m_NotificationBehavior = PlayerNotifications.InvokeUnityEvents;
158+
editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput);
159+
yield return RenderInspector();
160+
161+
var behaviorProperty = editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior));
162+
behaviorProperty.intValue = (int)PlayerNotifications.SendMessages;
163+
editor.serializedObject.ApplyModifiedPropertiesWithoutUndo();
164+
yield return RenderInspector();
165+
166+
LoadActions(asset, "first", "second");
167+
168+
behaviorProperty.intValue = (int)PlayerNotifications.InvokeUnityEvents;
169+
editor.serializedObject.ApplyModifiedPropertiesWithoutUndo();
170+
yield return RenderInspector();
171+
172+
Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId),
173+
Is.EquivalentTo(asset.Select(a => a.id.ToString())));
174+
}
175+
finally
176+
{
177+
Object.DestroyImmediate(asset);
178+
}
179+
}
180+
181+
// Behavior changes made through undo/redo don't go through the inspector's dropdown.
182+
[UnityTest]
183+
public IEnumerator ActionEventsAreListed_WhenInvokeUnityEventsIsRestoredByUndoRedo()
184+
{
185+
var asset = ScriptableObject.CreateInstance<InputActionAsset>();
186+
try
187+
{
188+
LoadActions(asset, "first");
189+
190+
playerInput.m_Actions = asset;
191+
playerInput.m_NotificationBehavior = PlayerNotifications.SendMessages;
192+
editor = (PlayerInputEditor)UnityEditor.Editor.CreateEditor(playerInput);
193+
yield return RenderInspector();
194+
195+
Undo.IncrementCurrentGroup();
196+
editor.serializedObject.FindProperty(nameof(PlayerInput.m_NotificationBehavior)).intValue =
197+
(int)PlayerNotifications.InvokeUnityEvents;
198+
editor.serializedObject.ApplyModifiedProperties();
199+
Undo.IncrementCurrentGroup();
200+
yield return RenderInspector();
201+
202+
Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId),
203+
Is.EquivalentTo(asset.Select(a => a.id.ToString())));
204+
205+
Undo.PerformUndo();
206+
yield return RenderInspector();
207+
Assert.That(playerInput.m_NotificationBehavior, Is.EqualTo(PlayerNotifications.SendMessages));
208+
209+
LoadActions(asset, "first", "second");
210+
211+
Undo.PerformRedo();
212+
yield return RenderInspector();
213+
Assert.That(playerInput.m_NotificationBehavior, Is.EqualTo(PlayerNotifications.InvokeUnityEvents));
214+
215+
Assert.That(playerInput.m_ActionEvents.Select(e => e.actionId),
216+
Is.EquivalentTo(asset.Select(a => a.id.ToString())));
217+
}
218+
finally
219+
{
220+
Object.DestroyImmediate(asset);
221+
}
222+
}
223+
}

‎Assets/Tests/InputSystem.Editor/PlayerInputEditorTests.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Packages/com.unity.inputsystem/CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1212
- Fixed polling for interaction phases (such as `WasPerformedThisFrame`) being missed during `Update` due to interaction timeouts sometimes occurring during the later before-render period when a before-render device (such as an XR HMD) is added. This means that timeouts and interactions like Hold may now wait to perform until the next frame instead of the earliest chance during before-render. [UUM-147719](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-147719)
1313
- Fixed touch input from Unity Remote 5 being ignored in Unity 6000.5 and newer. [UUM-155327](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-155327)
1414
- Fixed actions enabled during startup, for example in an Entities system's `OnCreate`, being disabled when entering Play Mode with domain reload disabled and in Player builds. [UUM-151403](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-151403)
15+
- Fixed the Player Input component Inspector not showing the action map events after the Behavior is set to Invoke Unity Events, until the GameObject was selected again. [UUM-150956](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-150956)
1516

1617
## [1.20.1] - 2026-10-05
1718

‎Packages/com.unity.inputsystem/InputSystem/Editor/Plugins/PlayerInput/PlayerInputEditor.cs‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ public override void OnInspectorGUI()
213213
// Notifications/event section.
214214
EditorGUI.BeginChangeCheck();
215215
EditorGUILayout.PropertyField(m_NotificationBehaviorProperty, m_NotificationBehaviorText);
216-
if (EditorGUI.EndChangeCheck() || actionsWereChanged || !m_NotificationBehaviorInitialized)
216+
// Also compare against the last handled behavior to catch changes made outside this inspector (undo, scripts).
217+
if (EditorGUI.EndChangeCheck() || actionsWereChanged || !m_NotificationBehaviorInitialized ||
218+
m_NotificationBehavior != (PlayerNotifications)m_NotificationBehaviorProperty.intValue)
217219
OnNotificationBehaviorChange();
218220
switch ((PlayerNotifications)m_NotificationBehaviorProperty.intValue)
219221
{
@@ -451,18 +453,18 @@ private void OnNotificationBehaviorChange()
451453
playerInput.m_DeviceRegainedEvent ??= new PlayerInput.DeviceRegainedEvent();
452454
playerInput.m_ControlsChangedEvent ??= new PlayerInput.ControlsChangedEvent();
453455

454-
if (areEventsDirty)
455-
{
456-
serializedObject.Update();
457-
458-
// Force action refresh.
459-
m_ActionAssetInitialized = false;
460-
Refresh();
461-
}
456+
// The action event list is only kept up to date while the behavior is InvokeUnityEvents, so rebuild
457+
// it whenever we switch to it. Do it right away rather than deferring to the next repaint, so the
458+
// events show up in this same GUI pass.
459+
var enteringUnityEvents = !m_NotificationBehaviorInitialized ||
460+
m_NotificationBehavior != PlayerNotifications.InvokeUnityEvents;
461+
if (areEventsDirty || enteringUnityEvents)
462+
InitializeEditorComponent(false);
462463
break;
463464
}
464465
}
465466

467+
m_NotificationBehavior = notificationBehavior;
466468
m_NotificationBehaviorInitialized = true;
467469
}
468470

@@ -655,6 +657,7 @@ void AddEntry(InputAction action, PlayerInput.ActionEvent actionEvent)
655657
[NonSerialized] private SerializedProperty m_ControlsChangedEventProperty;
656658

657659
[NonSerialized] private bool m_NotificationBehaviorInitialized;
660+
[NonSerialized] private PlayerNotifications m_NotificationBehavior;
658661
[NonSerialized] private bool m_ActionAssetInitialized;
659662
#if UNITY_6000_4_OR_NEWER
660663
[NonSerialized] private EntityId m_ActionAssetEntityId;

‎Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4547,6 +4547,7 @@ private static void RemovePlayModeCallbacks<TDelegate>(ref CallbackArray<TDelega
45474547
callbacks.RemoveCallback(callbacks[i]);
45484548
}
45494549
}
4550+
45504551
#endif
45514552

45524553
internal static ISavedState SaveAndResetState()

0 commit comments

Comments
 (0)