A game about forced loneliness, made by TACStudios
1using System;
2using UnityEngine.InputSystem.Utilities;
3using Unity.Collections.LowLevel.Unsafe;
4using UnityEngine.InputSystem.LowLevel;
5
6////REVIEW: for vector2 visualizers of sticks, it could be useful to also visualize deadzones and raw values
7
8namespace UnityEngine.InputSystem.Samples
9{
10 internal static class VisualizationHelpers
11 {
12 public enum Axis { X, Y, Z }
13
14 public abstract class Visualizer
15 {
16 public abstract void OnDraw(Rect rect);
17 public abstract void AddSample(object value, double time);
18 }
19
20 public abstract class ValueVisualizer<TValue> : Visualizer
21 where TValue : struct
22 {
23 public RingBuffer<TValue> samples;
24 public RingBuffer<GUIContent> samplesText;
25
26 protected ValueVisualizer(int numSamples = 10)
27 {
28 samples = new RingBuffer<TValue>(numSamples);
29 samplesText = new RingBuffer<GUIContent>(numSamples);
30 }
31
32 public override void AddSample(object value, double time)
33 {
34 var v = default(TValue);
35
36 if (value != null)
37 {
38 if (!(value is TValue val))
39 throw new ArgumentException(
40 $"Expecting value of type '{typeof(TValue).Name}' but value of type '{value?.GetType().Name}' instead",
41 nameof(value));
42 v = val;
43 }
44
45 samples.Append(v);
46 samplesText.Append(new GUIContent(v.ToString()));
47 }
48 }
49
50 // Visualizes integer and real type primitives.
51 public class ScalarVisualizer<TValue> : ValueVisualizer<TValue>
52 where TValue : struct
53 {
54 public TValue limitMin;
55 public TValue limitMax;
56 public TValue min;
57 public TValue max;
58
59 public ScalarVisualizer(int numSamples = 10)
60 : base(numSamples)
61 {
62 }
63
64 public override void OnDraw(Rect rect)
65 {
66 // For now, only draw the current value.
67 DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
68 if (samples.count == 0)
69 return;
70 var sample = samples[samples.count - 1];
71 if (Compare(sample, default) == 0)
72 return;
73 if (Compare(limitMin, default) != 0)
74 {
75 // Two-way visualization with positive and negative side.
76 throw new NotImplementedException();
77 }
78 else
79 {
80 // One-way visualization with only positive side.
81 var ratio = Divide(sample, limitMax);
82 var fillRect = rect;
83 fillRect.width = rect.width * ratio;
84 DrawRectangle(fillRect, new Color(0, 1, 0, 0.75f));
85
86 var valuePos = new Vector2(fillRect.xMax, fillRect.y + fillRect.height / 2);
87 DrawText(samplesText[samples.count - 1], valuePos, ValueTextStyle);
88 }
89 }
90
91 public override void AddSample(object value, double time)
92 {
93 base.AddSample(value, time);
94
95 if (value != null)
96 {
97 var val = (TValue)value;
98 if (Compare(min, val) > 0)
99 min = val;
100 if (Compare(max, val) < 0)
101 max = val;
102 }
103 }
104
105 private static unsafe int Compare(TValue left, TValue right)
106 {
107 var leftPtr = UnsafeUtility.AddressOf(ref left);
108 var rightPtr = UnsafeUtility.AddressOf(ref right);
109 if (typeof(TValue) == typeof(int))
110 return ((int*)leftPtr)->CompareTo(*(int*)rightPtr);
111 if (typeof(TValue) == typeof(float))
112 return ((float*)leftPtr)->CompareTo(*(float*)rightPtr);
113 throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
114 }
115
116 private static unsafe void Subtract(ref TValue left, TValue right)
117 {
118 var leftPtr = UnsafeUtility.AddressOf(ref left);
119 var rightPtr = UnsafeUtility.AddressOf(ref right);
120
121 if (typeof(TValue) == typeof(int))
122 *(int*)leftPtr = *(int*)leftPtr - *(int*)rightPtr;
123 if (typeof(TValue) == typeof(float))
124 *(float*)leftPtr = *(float*)leftPtr - *(float*)rightPtr;
125 throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
126 }
127
128 private static unsafe float Divide(TValue left, TValue right)
129 {
130 var leftPtr = UnsafeUtility.AddressOf(ref left);
131 var rightPtr = UnsafeUtility.AddressOf(ref right);
132
133 if (typeof(TValue) == typeof(int))
134 return (float)*(int*)leftPtr / *(int*)rightPtr;
135 if (typeof(TValue) == typeof(float))
136 return *(float*)leftPtr / *(float*)rightPtr;
137 throw new NotImplementedException("Scalar value type: " + typeof(TValue).Name);
138 }
139 }
140
141 // Visualizes .current device value
142 public class CurrentDeviceVisualizer : Visualizer
143 {
144 private InputDevice m_CurrentDevice = null;
145
146 public override void OnDraw(Rect rect)
147 {
148 // For now, only draw the current value.
149 DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
150
151 var name = m_CurrentDevice != null ? m_CurrentDevice.name : "null";
152 DrawText(name, new Vector2(rect.xMin + 4, (rect.yMin + rect.yMax) / 2.0f), ValueTextStyle);
153 }
154
155 public override void AddSample(object value, double time)
156 {
157 var device = (InputDevice)value;
158 if (device is Gamepad)
159 m_CurrentDevice = Gamepad.current;
160 else if (device is Mouse)
161 m_CurrentDevice = Mouse.current;
162 else if (device is Pen)
163 m_CurrentDevice = Pen.current;
164 else if (device is Pointer) // should be last, because it's a base class for Mouse and Pen
165 m_CurrentDevice = Pointer.current;
166 else
167 throw new ArgumentException(
168 $"Expected device type that implements .current, but got '{device.name}' (deviceId: {device.deviceId}) instead ");
169 }
170 }
171
172 ////TODO: allow asymmetric center (i.e. center not being a midpoint of rectangle)
173 ////TODO: enforce proper proportion between X and Y; it's confusing that X and Y can have different units yet have the same length
174 public class Vector2Visualizer : ValueVisualizer<Vector2>
175 {
176 // Our value space extends radially from the center, i.e. we have
177 // 360 discrete directions. Sampling at that granularity doesn't work
178 // super well in visualizations so we quantize to 3 degree increments.
179 public Vector2[] maximums = new Vector2[360 / 3];
180 public Vector2 limits = new Vector2(1, 1);
181
182 private GUIContent limitsXText;
183 private GUIContent limitsYText;
184
185 public Vector2Visualizer(int numSamples = 10)
186 : base(numSamples)
187 {
188 }
189
190 public override void AddSample(object value, double time)
191 {
192 base.AddSample(value, time);
193
194 if (value != null)
195 {
196 // Keep track of radial maximums.
197 var vector = (Vector2)value;
198 var angle = Vector2.SignedAngle(Vector2.right, vector);
199 if (angle < 0)
200 angle = 360 + angle;
201 var angleInt = Mathf.FloorToInt(angle) / 3;
202 if (vector.sqrMagnitude > maximums[angleInt].sqrMagnitude)
203 maximums[angleInt] = vector;
204
205 // Extend limits if value is out of range.
206 var limitX = Mathf.Max(Mathf.Abs(vector.x), limits.x);
207 var limitY = Mathf.Max(Mathf.Abs(vector.y), limits.y);
208 if (!Mathf.Approximately(limitX, limits.x))
209 {
210 limits.x = limitX;
211 limitsXText = null;
212 }
213 if (!Mathf.Approximately(limitY, limits.y))
214 {
215 limits.y = limitY;
216 limitsYText = null;
217 }
218 }
219 }
220
221 public override void OnDraw(Rect rect)
222 {
223 DrawRectangle(rect, new Color(1, 1, 1, 0.1f));
224 DrawAxis(Axis.X, rect, new Color(0, 1, 0, 0.75f));
225 DrawAxis(Axis.Y, rect, new Color(0, 1, 0, 0.75f));
226
227 var sampleCount = samples.count;
228 if (sampleCount == 0)
229 return;
230
231 // If limits aren't (1,1), show the actual values.
232 if (limits != new Vector2(1, 1))
233 {
234 if (limitsXText == null)
235 limitsXText = new GUIContent(limits.x.ToString());
236 if (limitsYText == null)
237 limitsYText = new GUIContent(limits.y.ToString());
238
239 var limitsXSize = ValueTextStyle.CalcSize(limitsXText);
240 var limitsXPos = new Vector2(rect.x - limitsXSize.x, rect.y - 5);
241 var limitsYPos = new Vector2(rect.xMax, rect.yMax);
242
243 DrawText(limitsXText, limitsXPos, ValueTextStyle);
244 DrawText(limitsYText, limitsYPos, ValueTextStyle);
245 }
246
247 // Draw maximums.
248 var numMaximums = 0;
249 var firstMaximumPos = default(Vector2);
250 var lastMaximumPos = default(Vector2);
251 for (var i = 0; i < 360 / 3; ++i)
252 {
253 var value = maximums[i];
254 if (value == default)
255 continue;
256 var valuePos = PixelPosForValue(value, rect);
257 if (numMaximums > 0)
258 DrawLine(lastMaximumPos, valuePos, new Color(1, 1, 1, 0.25f));
259 else
260 firstMaximumPos = valuePos;
261 lastMaximumPos = valuePos;
262 ++numMaximums;
263 }
264 if (numMaximums > 1)
265 DrawLine(lastMaximumPos, firstMaximumPos, new Color(1, 1, 1, 0.25f));
266
267 // Draw samples.
268 var alphaStep = 1f / sampleCount;
269 var alpha = 1f;
270 for (var i = sampleCount - 1; i >= 0; --i) // Go newest to oldest.
271 {
272 var value = samples[i];
273 var valueRect = RectForValue(value, rect);
274 DrawRectangle(valueRect, new Color(1, 0, 0, alpha));
275 alpha -= alphaStep;
276 }
277
278 // Print value of most recent sample. Draw last so
279 // we draw over the other stuff.
280 var lastSample = samples[sampleCount - 1];
281 var lastSamplePos = PixelPosForValue(lastSample, rect);
282 lastSamplePos.x += 3;
283 lastSamplePos.y += 3;
284 DrawText(samplesText[sampleCount - 1], lastSamplePos, ValueTextStyle);
285 }
286
287 private Rect RectForValue(Vector2 value, Rect rect)
288 {
289 var pos = PixelPosForValue(value, rect);
290 return new Rect(pos.x - 1, pos.y - 1, 2, 2);
291 }
292
293 private Vector2 PixelPosForValue(Vector2 value, Rect rect)
294 {
295 var center = rect.center;
296 var x = Mathf.Abs(value.x) / limits.x * Mathf.Sign(value.x);
297 var y = Mathf.Abs(value.y) / limits.y * Mathf.Sign(value.y) * -1; // GUI Y is upside down.
298 var xInPixels = x * rect.width / 2;
299 var yInPixels = y * rect.height / 2;
300 return new Vector2(center.x + xInPixels,
301 center.y + yInPixels);
302 }
303 }
304
305 // Y axis is time, X axis can be multiple visualizations.
306 public class TimelineVisualizer : Visualizer
307 {
308 public bool showLegend { get; set; }
309 public bool showLimits { get; set; }
310 public TimeUnit timeUnit { get; set; } = TimeUnit.Seconds;
311 public GUIContent valueUnit { get; set; }
312 ////REVIEW: should this be per timeline?
313 public int timelineCount => m_Timelines != null ? m_Timelines.Length : 0;
314 public int historyDepth { get; set; } = 100;
315
316 public Vector2 limitsY
317 {
318 get => m_LimitsY;
319 set
320 {
321 m_LimitsY = value;
322 m_LimitsYMin = null;
323 m_LimitsYMax = null;
324 }
325 }
326
327 public TimelineVisualizer(float totalTimeUnitsShown = 4)
328 {
329 m_TotalTimeUnitsShown = totalTimeUnitsShown;
330 }
331
332 public override void OnDraw(Rect rect)
333 {
334 var endTime = Time.realtimeSinceStartup;
335 var startTime = endTime - m_TotalTimeUnitsShown;
336 var endFrame = InputState.updateCount;
337 var startFrame = endFrame - (int)m_TotalTimeUnitsShown;
338
339 for (var i = 0; i < timelineCount; ++i)
340 {
341 var timeline = m_Timelines[i];
342 var sampleCount = timeUnit == TimeUnit.Frames
343 ? timeline.frameSamples.count
344 : timeline.timeSamples.count;
345
346 // Set up clip rect so that we can do stuff like render lines to samples
347 // falling outside the render rectangle and have them get clipped.
348 GUI.BeginGroup(rect);
349 var plotType = timeline.plotType;
350 var lastPos = default(Vector2);
351 var timeUnitsPerPixel = rect.width / m_TotalTimeUnitsShown;
352 var color = m_Timelines[i].color;
353 for (var n = sampleCount - 1; n >= 0; --n)
354 {
355 var sample = timeUnit == TimeUnit.Frames
356 ? timeline.frameSamples[n].value
357 : timeline.timeSamples[n].value;
358
359 ////TODO: respect limitsY
360
361 float y;
362 if (sample.isEmpty)
363 y = 0.5f;
364 else
365 y = sample.ToSingle();
366
367 y /= limitsY.y;
368
369 var deltaTime = timeUnit == TimeUnit.Frames
370 ? timeline.frameSamples[n].frame - startFrame
371 : timeline.timeSamples[n].time - startTime;
372 var pos = new Vector2(deltaTime * timeUnitsPerPixel, rect.height - y * rect.height);
373
374 if (plotType == PlotType.LineGraph)
375 {
376 if (n != sampleCount - 1)
377 {
378 DrawLine(lastPos, pos, color, 2);
379 if (pos.x < 0)
380 break;
381 }
382 }
383 else if (plotType == PlotType.BarChart)
384 {
385 ////TODO: make rectangles have a progressively stronger hue or saturation
386 var barRect = new Rect(pos.x, pos.y, timeUnitsPerPixel, y * limitsY.y * rect.height);
387 DrawRectangle(barRect, color);
388 }
389
390 lastPos = pos;
391 }
392 GUI.EndGroup();
393 }
394
395 if (showLegend && timelineCount > 0)
396 {
397 var legendRect = rect;
398 legendRect.x += rect.width + 2;
399 legendRect.width = 400;
400 legendRect.height = ValueTextStyle.CalcHeight(m_Timelines[0].name, 400);
401 for (var i = 0; i < m_Timelines.Length; ++i)
402 {
403 var colorTagRect = legendRect;
404 colorTagRect.width = 5;
405 var labelRect = legendRect;
406 labelRect.x += 8;
407 labelRect.width -= 8;
408
409 DrawRectangle(colorTagRect, m_Timelines[i].color);
410 DrawText(m_Timelines[i].name, labelRect.position, ValueTextStyle);
411
412 legendRect.y += labelRect.height + 2;
413 }
414 }
415
416 if (showLimits)
417 {
418 if (m_LimitsYMax == null)
419 m_LimitsYMax = new GUIContent(m_LimitsY.y.ToString());
420 if (m_LimitsYMin == null)
421 m_LimitsYMin = new GUIContent(m_LimitsY.x.ToString());
422
423 DrawText(m_LimitsYMax, new Vector2(rect.x + rect.width, rect.y), ValueTextStyle);
424 DrawText(m_LimitsYMin, new Vector2(rect.x + rect.width, rect.y + rect.height), ValueTextStyle);
425 }
426 }
427
428 public override void AddSample(object value, double time)
429 {
430 if (timelineCount == 0)
431 throw new InvalidOperationException("Must have set up a timeline first");
432 AddSample(0, PrimitiveValue.FromObject(value), (float)time);
433 }
434
435 public int AddTimeline(string name, Color color, PlotType plotType = default)
436 {
437 var timeline = new Timeline
438 {
439 name = new GUIContent(name),
440 color = color,
441 plotType = plotType,
442 };
443 if (timeUnit == TimeUnit.Frames)
444 timeline.frameSamples = new RingBuffer<FrameSample>(historyDepth);
445 else
446 timeline.timeSamples = new RingBuffer<TimeSample>(historyDepth);
447
448 var index = timelineCount;
449 Array.Resize(ref m_Timelines, timelineCount + 1);
450 m_Timelines[index] = timeline;
451
452 return index;
453 }
454
455 public int GetTimeline(string name)
456 {
457 for (var i = 0; i < timelineCount; ++i)
458 if (string.Compare(m_Timelines[i].name.text, name, StringComparison.InvariantCultureIgnoreCase) == 0)
459 return i;
460 return -1;
461 }
462
463 // Add a time-based sample.
464 public void AddSample(int timelineIndex, PrimitiveValue value, float time)
465 {
466 m_Timelines[timelineIndex].timeSamples.Append(new TimeSample
467 {
468 value = value,
469 time = time
470 });
471 }
472
473 // Add a frame-based sample.
474 public ref PrimitiveValue GetOrCreateSample(int timelineIndex, int frame)
475 {
476 ref var timeline = ref m_Timelines[timelineIndex];
477 ref var samples = ref timeline.frameSamples;
478 var count = samples.count;
479 if (count > 0)
480 {
481 if (samples[count - 1].frame == frame)
482 return ref samples[count - 1].value;
483
484 Debug.Assert(samples[count - 1].frame < frame, "Frame numbers must be ascending");
485 }
486
487 return ref samples.Append(new FrameSample {frame = frame}).value;
488 }
489
490 private float m_TotalTimeUnitsShown;
491 private Vector2 m_LimitsY = new Vector2(-1, 1);
492 private GUIContent m_LimitsYMin;
493 private GUIContent m_LimitsYMax;
494 private Timeline[] m_Timelines;
495
496 private struct TimeSample
497 {
498 public PrimitiveValue value;
499 public float time;
500 }
501
502 private struct FrameSample
503 {
504 public PrimitiveValue value;
505 public int frame;
506 }
507
508 private struct Timeline
509 {
510 public GUIContent name;
511 public Color color;
512 public RingBuffer<TimeSample> timeSamples;
513 public RingBuffer<FrameSample> frameSamples;
514 public PrimitiveValue minValue;
515 public PrimitiveValue maxValue;
516 public PlotType plotType;
517 }
518
519 public enum PlotType
520 {
521 LineGraph,
522 BarChart,
523 }
524
525 public enum TimeUnit
526 {
527 Seconds,
528 Frames,
529 }
530 }
531
532 public static void DrawAxis(Axis axis, Rect rect, Color color = default, float width = 1)
533 {
534 Vector2 start, end, tickOffset;
535 switch (axis)
536 {
537 case Axis.X:
538 start = new Vector2(rect.x, rect.y + rect.height / 2);
539 end = new Vector2(start.x + rect.width, rect.y + rect.height / 2);
540 tickOffset = new Vector2(0, 3);
541 break;
542
543 case Axis.Y:
544 start = new Vector2(rect.x + rect.width / 2, rect.y);
545 end = new Vector2(start.x, rect.y + rect.height);
546 tickOffset = new Vector2(3, 0);
547 break;
548
549 case Axis.Z:
550 // From bottom left corner to upper right corner.
551 start = new Vector2(rect.x, rect.yMax);
552 end = new Vector2(rect.xMax, rect.y);
553 tickOffset = new Vector2(1.5f, 1.5f);
554 break;
555
556 default:
557 throw new NotImplementedException();
558 }
559
560 ////TODO: label limits
561
562 DrawLine(start, end, color, width);
563 DrawLine(start - tickOffset, start + tickOffset, color, width);
564 DrawLine(end - tickOffset, end + tickOffset, color, width);
565 }
566
567 public static void DrawRectangle(Rect rect, Color color)
568 {
569 var savedColor = GUI.color;
570 GUI.color = color;
571 GUI.DrawTexture(rect, OnePixTex);
572 GUI.color = savedColor;
573 }
574
575 public static void DrawText(string text, Vector2 pos, GUIStyle style)
576 {
577 var content = new GUIContent(text);
578 DrawText(content, pos, style);
579 }
580
581 public static void DrawText(GUIContent text, Vector2 pos, GUIStyle style)
582 {
583 var content = new GUIContent(text);
584 var size = style.CalcSize(content);
585 var rect = new Rect(pos.x, pos.y, size.x, size.y);
586 style.Draw(rect, content, false, false, false, false);
587 }
588
589 // Adapted from http://wiki.unity3d.com/index.php?title=DrawLine
590 public static void DrawLine(Vector2 pointA, Vector2 pointB, Color color = default, float width = 1)
591 {
592 // Save the current GUI matrix, since we're going to make changes to it.
593 var matrix = GUI.matrix;
594
595 // Store current GUI color, so we can switch it back later,
596 // and set the GUI color to the color parameter
597 var savedColor = GUI.color;
598 GUI.color = color;
599
600 // Determine the angle of the line.
601 var angle = Vector3.Angle(pointB - pointA, Vector2.right);
602
603 // Vector3.Angle always returns a positive number.
604 // If pointB is above pointA, then angle needs to be negative.
605 if (pointA.y > pointB.y)
606 angle = -angle;
607
608 // Use ScaleAroundPivot to adjust the size of the line.
609 // We could do this when we draw the texture, but by scaling it here we can use
610 // non-integer values for the width and length (such as sub 1 pixel widths).
611 // Note that the pivot point is at +.5 from pointA.y, this is so that the width of the line
612 // is centered on the origin at pointA.
613 GUIUtility.ScaleAroundPivot(new Vector2((pointB - pointA).magnitude, width), new Vector2(pointA.x, pointA.y + 0.5f));
614
615 // Set the rotation for the line.
616 // The angle was calculated with pointA as the origin.
617 GUIUtility.RotateAroundPivot(angle, pointA);
618
619 // Finally, draw the actual line.
620 // We're really only drawing a 1x1 texture from pointA.
621 // The matrix operations done with ScaleAroundPivot and RotateAroundPivot will make this
622 // render with the proper width, length, and angle.
623 GUI.DrawTexture(new Rect(pointA.x, pointA.y, 1, 1), OnePixTex);
624
625 // We're done. Restore the GUI matrix and GUI color to whatever they were before.
626 GUI.matrix = matrix;
627 GUI.color = savedColor;
628 }
629
630 private static Texture2D s_OnePixTex;
631 private static GUIStyle s_ValueTextStyle;
632
633 internal static GUIStyle ValueTextStyle
634 {
635 get
636 {
637 if (s_ValueTextStyle == null)
638 {
639 s_ValueTextStyle = new GUIStyle();
640 s_ValueTextStyle.fontSize -= 2;
641 s_ValueTextStyle.normal.textColor = Color.white;
642 }
643 return s_ValueTextStyle;
644 }
645 }
646
647 internal static Texture2D OnePixTex
648 {
649 get
650 {
651 if (s_OnePixTex == null)
652 s_OnePixTex = new Texture2D(1, 1);
653 return s_OnePixTex;
654 }
655 }
656
657 public struct RingBuffer<TValue>
658 {
659 public TValue[] array;
660 public int head;
661 public int count;
662
663 public RingBuffer(int size)
664 {
665 array = new TValue[size];
666 head = 0;
667 count = 0;
668 }
669
670 public ref TValue Append(TValue value)
671 {
672 int index;
673 var bufferSize = array.Length;
674 if (count < bufferSize)
675 {
676 Debug.Assert(head == 0, "Head can't have moved if buffer isn't full yet");
677 index = count;
678 ++count;
679 }
680 else
681 {
682 // Buffer is full. Bump head.
683 index = (head + count) % bufferSize;
684 ++head;
685 }
686 array[index] = value;
687 return ref array[index];
688 }
689
690 public ref TValue this[int index]
691 {
692 get
693 {
694 if (index < 0 || index >= count)
695 throw new ArgumentOutOfRangeException(nameof(index));
696 return ref array[(head + index) % array.Length];
697 }
698 }
699 }
700 }
701}