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.

209 lines
6.6 KiB

  1. using Microsoft.Xna.Framework;
  2. using System;
  3. // Design largely from https://noonat.github.io/intersect/.
  4. namespace SemiColinGames {
  5. public static class Geometry {
  6. public static Vector2 Rotate(this Vector2 point, float angle) {
  7. float cos = FMath.Cos(angle);
  8. float sin = FMath.Sin(angle);
  9. return new Vector2(
  10. point.X * cos - point.Y * sin,
  11. point.Y * cos + point.X * sin);
  12. }
  13. }
  14. // Math functions that return floats rather than doubles, for convenience.
  15. public static class FMath {
  16. public const float PI = (float) Math.PI;
  17. public static float DegToRad(float degrees) {
  18. return PI / 180 * degrees;
  19. }
  20. public static float Sin(double degrees) {
  21. return (float) Math.Sin(degrees);
  22. }
  23. public static float Cos(double degrees) {
  24. return (float) Math.Cos(degrees);
  25. }
  26. public static T Clamp<T>(T value, T min, T max) where T : IComparable {
  27. if (value.CompareTo(min) == -1) {
  28. return min;
  29. } else if (value.CompareTo(max) == 1) {
  30. return max;
  31. } else {
  32. return value;
  33. }
  34. }
  35. }
  36. public readonly struct Hit {
  37. public readonly AABB Collider;
  38. public readonly Vector2 Position;
  39. public readonly Vector2 Delta;
  40. public readonly Vector2 Normal;
  41. public readonly float Time; // ranges from [0, 1].
  42. public Hit(AABB collider, Vector2 position, Vector2 delta, Vector2 normal) :
  43. this(collider, position, delta, normal, 0.0f) {
  44. }
  45. public Hit(AABB collider, Vector2 position, Vector2 delta, Vector2 normal, float time) {
  46. Collider = collider;
  47. Position = position;
  48. Delta = delta;
  49. Normal = normal;
  50. Time = time;
  51. }
  52. }
  53. public readonly struct Sweep {
  54. public readonly Hit? Hit;
  55. public readonly Vector2 Position;
  56. public readonly float Time;
  57. public Sweep(Hit? hit, Vector2 position, float time) {
  58. Hit = hit;
  59. Position = position;
  60. Time = time;
  61. }
  62. }
  63. public readonly struct AABB {
  64. public readonly Vector2 Position; // centroid
  65. public readonly Vector2 HalfSize;
  66. public AABB(Vector2 position, Vector2 halfSize) {
  67. Position = position;
  68. HalfSize = halfSize;
  69. }
  70. public float Top {
  71. get { return Position.Y - HalfSize.Y; }
  72. }
  73. public float Bottom {
  74. get { return Position.Y + HalfSize.Y; }
  75. }
  76. public float Left {
  77. get { return Position.X - HalfSize.X; }
  78. }
  79. public float Right {
  80. get { return Position.X + HalfSize.X; }
  81. }
  82. public Vector2 TopLeft {
  83. get { return new Vector2(Left, Top); }
  84. }
  85. public Vector2 TopRight {
  86. get { return new Vector2(Right, Top); }
  87. }
  88. public Vector2 BottomLeft {
  89. get { return new Vector2(Left, Bottom); }
  90. }
  91. public Vector2 BottomRight {
  92. get { return new Vector2(Right, Bottom); }
  93. }
  94. public Hit? Intersect(AABB box) {
  95. float dx = box.Position.X - Position.X;
  96. float px = box.HalfSize.X + HalfSize.X - Math.Abs(dx);
  97. if (px <= 0) {
  98. return null;
  99. }
  100. float dy = box.Position.Y - Position.Y;
  101. float py = box.HalfSize.Y + HalfSize.Y - Math.Abs(dy);
  102. if (py <= 0) {
  103. return null;
  104. }
  105. // TODO: which of delta/normal/hitPos do we actually care about?
  106. if (px < py) {
  107. int sign = Math.Sign(dx);
  108. Vector2 delta = new Vector2(px * sign, 0);
  109. Vector2 normal = new Vector2(sign, 0);
  110. Vector2 hitPos = new Vector2(Position.X + HalfSize.X * sign, box.Position.Y);
  111. return new Hit(box, hitPos, delta, normal);
  112. } else {
  113. int sign = Math.Sign(dy);
  114. Vector2 delta = new Vector2(0, py * sign);
  115. Vector2 normal = new Vector2(0, sign);
  116. Vector2 hitPos = new Vector2(box.Position.X, Position.Y + HalfSize.Y * sign);
  117. return new Hit(this, hitPos, delta, normal);
  118. }
  119. }
  120. public Hit? IntersectSegment(Vector2 pos, Vector2 delta) {
  121. return IntersectSegment(pos, delta, Vector2.Zero);
  122. }
  123. public Hit? IntersectSegment(Vector2 pos, Vector2 delta, Vector2 padding) {
  124. float scaleX = 1.0f / delta.X;
  125. float scaleY = 1.0f / delta.Y;
  126. int signX = Math.Sign(scaleX);
  127. int signY = Math.Sign(scaleY);
  128. float nearTimeX = (Position.X - signX * (HalfSize.X + padding.X) - pos.X) * scaleX;
  129. float nearTimeY = (Position.Y - signY * (HalfSize.Y + padding.Y) - pos.Y) * scaleY;
  130. float farTimeX = (Position.X + signX * (HalfSize.X + padding.X) - pos.X) * scaleX;
  131. float farTimeY = (Position.Y + signY * (HalfSize.Y + padding.Y) - pos.Y) * scaleY;
  132. if (nearTimeX > farTimeY || nearTimeY > farTimeX) {
  133. return null;
  134. }
  135. float nearTime = Math.Max(nearTimeX, nearTimeY);
  136. float farTime = Math.Min(farTimeX, farTimeY);
  137. if (nearTime >= 1 || farTime <= 0) {
  138. return null;
  139. }
  140. // If we've gotten this far, a collision is happening. If the near time is greater than zero,
  141. // the segment starts outside and is entering the box. Otherwise, the segment starts inside
  142. // the box, so we set the hit time to zero.
  143. float hitTime = Math.Max(0, nearTime);
  144. Vector2 normal = nearTimeX > nearTimeY ?
  145. new Vector2(-signX, 0) :
  146. new Vector2(0, -signY);
  147. // TODO: replace these with Vector2.Multiply (etc)
  148. Vector2 hitDelta = new Vector2((1.0f - hitTime) * -delta.X, (1.0f - hitTime) * -delta.Y);
  149. Vector2 hitPos = new Vector2(pos.X + delta.X * hitTime, pos.Y + delta.Y * hitTime);
  150. return new Hit(this, hitPos, hitDelta, normal, hitTime);
  151. }
  152. public Sweep Sweep(AABB box, Vector2 delta) {
  153. // fast-path case if the other box is static
  154. if (delta.X == 0 && delta.Y == 0) {
  155. Hit? staticHit = Intersect(box);
  156. // TODO: I don't understand the original source here, but I think this is correct.
  157. return new Sweep(staticHit, box.Position, staticHit?.Time ?? 1);
  158. }
  159. Hit? maybeHit = IntersectSegment(box.Position, delta, box.HalfSize);
  160. if (maybeHit == null) {
  161. return new Sweep(null, Vector2.Add(box.Position, delta), 1);
  162. }
  163. Hit hit = (Hit) maybeHit;
  164. Vector2 hitPos = new Vector2(
  165. box.Position.X + delta.X * hit.Time,
  166. box.Position.Y + delta.Y * hit.Time);
  167. Vector2 direction = Vector2.Normalize(delta);
  168. // TODO: why is this calculation made, and then thrown away?
  169. Vector2 sweepHitPos = new Vector2(
  170. FMath.Clamp(hit.Position.X + direction.X * box.HalfSize.X,
  171. Position.X - HalfSize.X,
  172. Position.X + HalfSize.X),
  173. FMath.Clamp(hit.Position.Y + direction.Y * box.HalfSize.Y,
  174. Position.Y - HalfSize.Y,
  175. Position.Y + HalfSize.Y));
  176. return new Sweep(hit, hitPos, hit.Time);
  177. }
  178. }
  179. }