A game about forced loneliness, made by TACStudios
at master 701 lines 28 kB view raw
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}