A stealth-based 2D platformer where you don't have to kill anyone unless you want to. https://www.semicolin.games
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
5.2 KiB

  1. using Microsoft.Xna.Framework;
  2. using System;
  3. // Design largely from https://noonat.github.io/intersect/.
  4. namespace SemiColinGames {
  5. public readonly struct Hit {
  6. public readonly AABB Collider;
  7. public readonly Vector2 Position;
  8. public readonly Vector2 Delta;
  9. public readonly Vector2 Normal;
  10. public readonly float Time; // ranges from [0, 1].
  11. public Hit(AABB collider, Vector2 position, Vector2 delta, Vector2 normal) :
  12. this(collider, position, delta, normal, 0.0f) {
  13. }
  14. public Hit(AABB collider, Vector2 position, Vector2 delta, Vector2 normal, float time) {
  15. Collider = collider;
  16. Position = position;
  17. Delta = delta;
  18. Normal = normal;
  19. Time = time;
  20. }
  21. }
  22. public readonly struct Sweep {
  23. public readonly Hit? Hit;
  24. public readonly Vector2 Position;
  25. public readonly float Time;
  26. public Sweep(Hit? hit, Vector2 position, float time) {
  27. Hit = hit;
  28. Position = position;
  29. Time = time;
  30. }
  31. }
  32. public readonly struct AABB {
  33. public readonly Vector2 Position; // centroid
  34. public readonly Vector2 HalfSize;
  35. static float Clamp(float value, float min, float max) {
  36. if (value < min) {
  37. return min;
  38. } else if (value > max) {
  39. return max;
  40. } else {
  41. return value;
  42. }
  43. }
  44. public AABB(Vector2 position, Vector2 halfSize) {
  45. Position = position;
  46. HalfSize = halfSize;
  47. }
  48. public Hit? Intersect(AABB box) {
  49. float dx = box.Position.X - Position.X;
  50. float px = box.HalfSize.X + HalfSize.X - Math.Abs(dx);
  51. if (px <= 0) {
  52. return null;
  53. }
  54. float dy = box.Position.Y - Position.Y;
  55. float py = box.HalfSize.Y + HalfSize.Y - Math.Abs(dy);
  56. if (py <= 0) {
  57. return null;
  58. }
  59. // TODO: which of delta/normal/hitPos do we actually care about?
  60. if (px < py) {
  61. int sign = Math.Sign(dx);
  62. Vector2 delta = new Vector2(px * sign, 0);
  63. Vector2 normal = new Vector2(sign, 0);
  64. Vector2 hitPos = new Vector2(Position.X + HalfSize.X * sign, box.Position.Y);
  65. return new Hit(box, hitPos, delta, normal);
  66. } else {
  67. int sign = Math.Sign(dy);
  68. Vector2 delta = new Vector2(0, py * sign);
  69. Vector2 normal = new Vector2(0, sign);
  70. Vector2 hitPos = new Vector2(box.Position.X, Position.Y + HalfSize.Y * sign);
  71. return new Hit(this, hitPos, delta, normal);
  72. }
  73. }
  74. public Hit? IntersectSegment(Vector2 pos, Vector2 delta) {
  75. return IntersectSegment(pos, delta, Vector2.Zero);
  76. }
  77. public Hit? IntersectSegment(Vector2 pos, Vector2 delta, Vector2 padding) {
  78. float scaleX = 1.0f / delta.X;
  79. float scaleY = 1.0f / delta.Y;
  80. int signX = Math.Sign(scaleX);
  81. int signY = Math.Sign(scaleY);
  82. float nearTimeX = (Position.X - signX * (HalfSize.X + padding.X) - pos.X) * scaleX;
  83. float nearTimeY = (Position.Y - signY * (HalfSize.Y + padding.Y) - pos.Y) * scaleY;
  84. float farTimeX = (Position.X + signX * (HalfSize.X + padding.X) - pos.X) * scaleX;
  85. float farTimeY = (Position.Y + signY * (HalfSize.Y + padding.Y) - pos.Y) * scaleY;
  86. if (nearTimeX > farTimeY || nearTimeY > farTimeX) {
  87. return null;
  88. }
  89. float nearTime = Math.Max(nearTimeX, nearTimeY);
  90. float farTime = Math.Min(farTimeX, farTimeY);
  91. if (nearTime >= 1 || farTime <= 0) {
  92. return null;
  93. }
  94. // If we've gotten this far, a collision is happening. If the near time is greater than zero,
  95. // the segment starts outside and is entering the box. Otherwise, the segment starts inside
  96. // the box, so we set the hit time to zero.
  97. float hitTime = Math.Max(0, nearTime);
  98. Vector2 normal = nearTimeX > nearTimeY ?
  99. new Vector2(-signX, 0) :
  100. new Vector2(0, -signY);
  101. // TODO: replace these with Vector2.Multiply (etc)
  102. Vector2 hitDelta = new Vector2((1.0f - hitTime) * -delta.X, (1.0f - hitTime) * -delta.Y);
  103. Vector2 hitPos = new Vector2(pos.X + delta.X * hitTime, pos.Y + delta.Y * hitTime);
  104. return new Hit(this, hitPos, hitDelta, normal, hitTime);
  105. }
  106. public Sweep Sweep(AABB box, Vector2 delta) {
  107. // fast-path case if the other box is static
  108. if (delta.X == 0 && delta.Y == 0) {
  109. Hit? staticHit = Intersect(box);
  110. // TODO: I don't understand the original source here, but I think this is correct.
  111. return new Sweep(staticHit, box.Position, staticHit?.Time ?? 1);
  112. }
  113. Hit? maybeHit = IntersectSegment(box.Position, delta, box.HalfSize);
  114. if (maybeHit == null) {
  115. return new Sweep(null, Vector2.Add(box.Position, delta), 1);
  116. }
  117. Hit hit = (Hit) maybeHit;
  118. Vector2 hitPos = new Vector2(
  119. box.Position.X + delta.X * hit.Time,
  120. box.Position.Y + delta.Y * hit.Time);
  121. Vector2 direction = Vector2.Normalize(delta);
  122. // TODO: why is this calculation made, and then thrown away?
  123. Vector2 sweepHitPos = new Vector2(
  124. Clamp(hit.Position.X + direction.X * box.HalfSize.X,
  125. Position.X - HalfSize.X,
  126. Position.X + HalfSize.X),
  127. Clamp(hit.Position.Y + direction.Y * box.HalfSize.Y,
  128. Position.Y - HalfSize.Y,
  129. Position.Y + HalfSize.Y));
  130. return new Sweep(hit, hitPos, hit.Time);
  131. }
  132. }
  133. }