A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEngine.InputSystem.Layouts;
4using UnityEngine.InputSystem.LowLevel;
5
6////TODO: add way to plot values over time
7
8// Goal is to build this out into something that can visualize a large number of
9// aspects about an InputControl/InputDevice especially with an eye towards making
10// it a good deal to debug any input collection/processing irregularities that may
11// be seen in players (or the editor, for that matter).
12
13// Some fields assigned through only through serialization.
14#pragma warning disable CS0649
15
16namespace UnityEngine.InputSystem.Samples
17{
18 /// <summary>
19 /// A component for debugging purposes that adds an on-screen display which shows
20 /// activity on an input control over time.
21 /// </summary>
22 /// <remarks>
23 /// This component is most useful for debugging input directly on the source device.
24 /// </remarks>
25 /// <seealso cref="InputActionVisualizer"/>
26 [AddComponentMenu("Input/Debug/Input Control Visualizer")]
27 [ExecuteInEditMode]
28 public class InputControlVisualizer : InputVisualizer
29 {
30 /// <summary>
31 /// What kind of visualization to show.
32 /// </summary>
33 public Mode visualization
34 {
35 get => m_Visualization;
36 set
37 {
38 if (m_Visualization == value)
39 return;
40 m_Visualization = value;
41 SetupVisualizer();
42 }
43 }
44
45 /// <summary>
46 /// Path of the control that is to be visualized.
47 /// </summary>
48 /// <seealso cref="InputControlPath"/>
49 /// <seealso cref="InputControl.path"/>
50 public string controlPath
51 {
52 get => m_ControlPath;
53 set
54 {
55 m_ControlPath = value;
56 if (m_Control != null)
57 ResolveControl();
58 }
59 }
60
61 /// <summary>
62 /// If, at runtime, multiple controls are matching <see cref="controlPath"/>, this property
63 /// determines the index of the control that is retrieved from the possible options.
64 /// </summary>
65 public int controlIndex
66 {
67 get => m_ControlIndex;
68 set
69 {
70 m_ControlIndex = value;
71 if (m_Control != null)
72 ResolveControl();
73 }
74 }
75
76 /// <summary>
77 /// The control resolved from <see cref="controlPath"/> at runtime. May be null.
78 /// </summary>
79 public InputControl control => m_Control;
80
81 protected new void OnEnable()
82 {
83 if (m_Visualization == Mode.None)
84 return;
85
86 if (s_EnabledInstances == null)
87 s_EnabledInstances = new List<InputControlVisualizer>();
88 if (s_EnabledInstances.Count == 0)
89 {
90 InputSystem.onDeviceChange += OnDeviceChange;
91 InputSystem.onEvent += OnEvent;
92 }
93 s_EnabledInstances.Add(this);
94
95 ResolveControl();
96
97 base.OnEnable();
98 }
99
100 protected new void OnDisable()
101 {
102 if (m_Visualization == Mode.None)
103 return;
104
105 if (s_EnabledInstances != null)
106 {
107 s_EnabledInstances.Remove(this);
108 if (s_EnabledInstances.Count == 0)
109 {
110 InputSystem.onDeviceChange -= OnDeviceChange;
111 InputSystem.onEvent -= OnEvent;
112 }
113 }
114
115 m_Control = null;
116
117 base.OnDisable();
118 }
119
120 protected new void OnGUI()
121 {
122 if (m_Visualization == Mode.None)
123 return;
124
125 base.OnGUI();
126 }
127
128 protected new void OnValidate()
129 {
130 ResolveControl();
131 base.OnValidate();
132 }
133
134 [Tooltip("The type of visualization to perform for the control.")]
135 [SerializeField] private Mode m_Visualization;
136 [Tooltip("Path of the control that should be visualized. If at runtime, multiple "
137 + "controls match the given path, the 'Control Index' property can be used to decide "
138 + "which of the controls to visualize.")]
139 [InputControl, SerializeField] private string m_ControlPath;
140 [Tooltip("If multiple controls match 'Control Path' at runtime, this property decides "
141 + "which control to visualize from the list of candidates. It is a zero-based index.")]
142 [SerializeField] private int m_ControlIndex;
143
144 [NonSerialized] private InputControl m_Control;
145
146 private static List<InputControlVisualizer> s_EnabledInstances;
147
148 private void ResolveControl()
149 {
150 m_Control = null;
151 if (string.IsNullOrEmpty(m_ControlPath))
152 return;
153
154 using (var candidates = InputSystem.FindControls(m_ControlPath))
155 {
156 var numCandidates = candidates.Count;
157 if (numCandidates > 1 && m_ControlIndex < numCandidates && m_ControlIndex >= 0)
158 m_Control = candidates[m_ControlIndex];
159 else if (numCandidates > 0)
160 m_Control = candidates[0];
161 }
162
163 SetupVisualizer();
164 }
165
166 private void SetupVisualizer()
167 {
168 if (m_Control == null)
169 {
170 m_Visualizer = null;
171 return;
172 }
173
174 switch (m_Visualization)
175 {
176 case Mode.Value:
177 {
178 var valueType = m_Control.valueType;
179 if (valueType == typeof(Vector2))
180 m_Visualizer = new VisualizationHelpers.Vector2Visualizer(m_HistorySamples);
181 else if (valueType == typeof(float))
182 m_Visualizer = new VisualizationHelpers.ScalarVisualizer<float>(m_HistorySamples)
183 {
184 ////TODO: pass actual min/max limits of control
185 limitMax = 1,
186 limitMin = 0
187 };
188 else if (valueType == typeof(int))
189 m_Visualizer = new VisualizationHelpers.ScalarVisualizer<int>(m_HistorySamples)
190 {
191 ////TODO: pass actual min/max limits of control
192 limitMax = 1,
193 limitMin = 0
194 };
195 else
196 {
197 ////TODO: generic visualizer
198 }
199 break;
200 }
201
202 case Mode.Events:
203 {
204 var visualizer = new VisualizationHelpers.TimelineVisualizer(m_HistorySamples)
205 {
206 timeUnit = VisualizationHelpers.TimelineVisualizer.TimeUnit.Frames,
207 historyDepth = m_HistorySamples,
208 showLimits = true,
209 limitsY = new Vector2(0, 5) // Will expand upward automatically
210 };
211 m_Visualizer = visualizer;
212 visualizer.AddTimeline("Events", Color.green,
213 VisualizationHelpers.TimelineVisualizer.PlotType.BarChart);
214 break;
215 }
216
217 case Mode.MaximumLag:
218 {
219 var visualizer = new VisualizationHelpers.TimelineVisualizer(m_HistorySamples)
220 {
221 timeUnit = VisualizationHelpers.TimelineVisualizer.TimeUnit.Frames,
222 historyDepth = m_HistorySamples,
223 valueUnit = new GUIContent("ms"),
224 showLimits = true,
225 limitsY = new Vector2(0, 6)
226 };
227 m_Visualizer = visualizer;
228 visualizer.AddTimeline("MaxLag", Color.red,
229 VisualizationHelpers.TimelineVisualizer.PlotType.BarChart);
230 break;
231 }
232
233 case Mode.Bytes:
234 {
235 var visualizer = new VisualizationHelpers.TimelineVisualizer(m_HistorySamples)
236 {
237 timeUnit = VisualizationHelpers.TimelineVisualizer.TimeUnit.Frames,
238 valueUnit = new GUIContent("bytes"),
239 historyDepth = m_HistorySamples,
240 showLimits = true,
241 limitsY = new Vector2(0, 64)
242 };
243 m_Visualizer = visualizer;
244 visualizer.AddTimeline("Bytes", Color.red,
245 VisualizationHelpers.TimelineVisualizer.PlotType.BarChart);
246 break;
247 }
248
249 case Mode.DeviceCurrent:
250 {
251 m_Visualizer = new VisualizationHelpers.CurrentDeviceVisualizer();
252 break;
253 }
254
255 default:
256 throw new NotImplementedException();
257 }
258 }
259
260 private static void OnDeviceChange(InputDevice device, InputDeviceChange change)
261 {
262 if (change != InputDeviceChange.Added && change != InputDeviceChange.Removed)
263 return;
264
265 for (var i = 0; i < s_EnabledInstances.Count; ++i)
266 {
267 var component = s_EnabledInstances[i];
268 if (change == InputDeviceChange.Removed && component.m_Control != null &&
269 component.m_Control.device == device)
270 component.ResolveControl();
271 else if (change == InputDeviceChange.Added)
272 component.ResolveControl();
273 }
274 }
275
276 private static void OnEvent(InputEventPtr eventPtr, InputDevice device)
277 {
278 // Ignore very first update as we usually get huge lag spikes and event count
279 // spikes in it from stuff that has accumulated while going into play mode or
280 // starting up the player.
281 if (InputState.updateCount <= 1)
282 return;
283
284 if (InputState.currentUpdateType == InputUpdateType.Editor)
285 return;
286
287 if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>())
288 return;
289
290 for (var i = 0; i < s_EnabledInstances.Count; ++i)
291 {
292 var component = s_EnabledInstances[i];
293 if (component.m_Control?.device != device || component.m_Visualizer == null)
294 continue;
295
296 component.OnEventImpl(eventPtr, device);
297 }
298 }
299
300 private unsafe void OnEventImpl(InputEventPtr eventPtr, InputDevice device)
301 {
302 switch (m_Visualization)
303 {
304 case Mode.Value:
305 {
306 var statePtr = m_Control.GetStatePtrFromStateEvent(eventPtr);
307 if (statePtr == null)
308 return; // No value for control in event.
309 var value = m_Control.ReadValueFromStateAsObject(statePtr);
310 m_Visualizer.AddSample(value, eventPtr.time);
311 break;
312 }
313
314 case Mode.Events:
315 {
316 var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer;
317 var frame = (int)InputState.updateCount;
318 ref var valueRef = ref visualizer.GetOrCreateSample(0, frame);
319 var value = valueRef.ToInt32() + 1;
320 valueRef = value;
321 visualizer.limitsY =
322 new Vector2(0, Mathf.Max(value, visualizer.limitsY.y));
323 break;
324 }
325
326 case Mode.MaximumLag:
327 {
328 var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer;
329 var lag = (Time.realtimeSinceStartup - eventPtr.time) * 1000; // In milliseconds.
330 var frame = (int)InputState.updateCount;
331 ref var valueRef = ref visualizer.GetOrCreateSample(0, frame);
332
333 if (lag > valueRef.ToDouble())
334 {
335 valueRef = lag;
336 if (lag > visualizer.limitsY.y)
337 visualizer.limitsY = new Vector2(0, Mathf.Ceil((float)lag));
338 }
339 break;
340 }
341
342 case Mode.Bytes:
343 {
344 var visualizer = (VisualizationHelpers.TimelineVisualizer)m_Visualizer;
345 var frame = (int)InputState.updateCount;
346 ref var valueRef = ref visualizer.GetOrCreateSample(0, frame);
347 var value = valueRef.ToInt32() + eventPtr.sizeInBytes;
348 valueRef = value;
349 visualizer.limitsY =
350 new Vector2(0, Mathf.Max(value, visualizer.limitsY.y));
351 break;
352 }
353
354 case Mode.DeviceCurrent:
355 {
356 m_Visualizer.AddSample(device, eventPtr.time);
357 break;
358 }
359 }
360 }
361
362 /// <summary>
363 /// Determines which aspect of the control should be visualized.
364 /// </summary>
365 public enum Mode
366 {
367 None = 0,
368 Value = 1,
369 Events = 4,
370 MaximumLag = 6,
371 Bytes = 7,
372 DeviceCurrent = 8,
373 }
374 }
375}