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.

266 lines
8.1 KiB

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