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.

307 lines
10 KiB

  1. using Microsoft.Xna.Framework;
  2. using Microsoft.Xna.Framework.Graphics;
  3. using System;
  4. using System.Collections.Generic;
  5. namespace SemiColinGames {
  6. class Player {
  7. // The player's Facing corresponds to the x-direction that they're looking.
  8. enum Facing {
  9. Left = -1,
  10. Right = 1
  11. };
  12. enum Pose { Walking, Standing, Crouching, Stretching, SwordSwing, Jumping };
  13. private const int moveSpeed = 180;
  14. private const int jumpSpeed = -600;
  15. private const int gravity = 2400;
  16. // Details of the sprite image.
  17. // player_1x is 48 x 48, yOffset=5, halfSize=(7, 14)
  18. // Ninja_Female is 96 x 64, yOffset=1, halfSize=(11, 24)
  19. private const int spriteWidth = 96;
  20. private const int spriteHeight = 64;
  21. private const int spriteCenterYOffset = 1;
  22. private readonly Texture2D texture;
  23. // Details of the actual Player model.
  24. // Position is tracked at the Player's center. The Player's bounding box is a rectangle
  25. // centered at that point and extending out by halfSize.X and halfSize.Y.
  26. private Point position = new Point(64, 16 * 13);
  27. private Vector2 halfSize = new Vector2(11, 24);
  28. private Vector2 eyeOffsetStanding = new Vector2(7, -14);
  29. private Vector2 eyeOffsetWalking = new Vector2(15, -7);
  30. private int jumps = 0;
  31. private Facing facing = Facing.Right;
  32. private Pose pose = Pose.Jumping;
  33. private double swordSwingTime = 0;
  34. private int swordSwingNum = 0;
  35. private const int swordSwingMax = 6;
  36. private float ySpeed = 0;
  37. public Player(Texture2D texture) {
  38. this.texture = texture;
  39. }
  40. public Point Position { get { return position; } }
  41. public void Update(float modelTime, History<Input> input, AABB[] collisionTargets) {
  42. AABB BoxOffset(Point position, int yOffset) {
  43. return new AABB(new Vector2(position.X, position.Y + yOffset), halfSize);
  44. }
  45. AABB Box(Point position) {
  46. return BoxOffset(position, 0);
  47. }
  48. Vector2 movement = HandleInput(modelTime, input);
  49. // Broad test: remove all collision targets nowhere near the player.
  50. // TODO: don't allocate a list here.
  51. var candidates = new List<AABB>();
  52. // Expand the box in the direction of movement. The center is the midpoint of the line
  53. // between the player's current position and their desired movement. The width increases by
  54. // the magnitude of the movement in each direction. We add 1 to each dimension just to be
  55. // sure (the only downside is a small number of false-positive AABBs, which should be
  56. // discarded by later tests anyhow.)
  57. AABB largeBox = new AABB(
  58. new Vector2(position.X + movement.X / 2, position.Y + movement.Y / 2),
  59. new Vector2(halfSize.X + Math.Abs(movement.X) + 1, halfSize.Y + Math.Abs(movement.Y) + 1));
  60. foreach (var box in collisionTargets) {
  61. if (box.Intersect(largeBox) != null) {
  62. // Debug.AddRect(box, Color.Green);
  63. candidates.Add(box);
  64. }
  65. }
  66. Point[] movePoints = Line.Rasterize(0, 0, (int) movement.X, (int) movement.Y);
  67. for (int i = 1; i < movePoints.Length; i++) {
  68. int dx = movePoints[i].X - movePoints[i - 1].X;
  69. int dy = movePoints[i].Y - movePoints[i - 1].Y;
  70. if (dy != 0) {
  71. Point newPosition = new Point(position.X, position.Y + dy);
  72. AABB player = Box(newPosition);
  73. bool reject = false;
  74. foreach (var box in candidates) {
  75. if (box.Intersect(player) != null) {
  76. reject = true;
  77. break;
  78. }
  79. }
  80. if (!reject) {
  81. position = newPosition;
  82. }
  83. }
  84. if (dx != 0) {
  85. Point newPosition = new Point(position.X + dx, position.Y);
  86. AABB player = Box(newPosition);
  87. bool reject = false;
  88. foreach (var box in candidates) {
  89. if (box.Intersect(player) != null) {
  90. reject = true;
  91. break;
  92. }
  93. }
  94. if (!reject) {
  95. position = newPosition;
  96. }
  97. }
  98. }
  99. bool standingOnGround = false;
  100. AABB groundIntersect = BoxOffset(position, 1);
  101. foreach (var box in candidates) {
  102. if (groundIntersect.Intersect(box) != null) {
  103. standingOnGround = true;
  104. break;
  105. }
  106. }
  107. if (standingOnGround) {
  108. jumps = 1;
  109. ySpeed = -0.0001f;
  110. // Debug.AddRect(Box(position), Color.Cyan);
  111. } else {
  112. jumps = 0;
  113. // Debug.AddRect(Box(position), Color.Orange);
  114. }
  115. if (movement.X > 0) {
  116. facing = Facing.Right;
  117. } else if (movement.X < 0) {
  118. facing = Facing.Left;
  119. }
  120. if (swordSwingTime > 0) {
  121. pose = Pose.SwordSwing;
  122. } else if (jumps == 0) {
  123. pose = Pose.Jumping;
  124. } else if (movement.X != 0) {
  125. pose = Pose.Walking;
  126. } else if (input[0].Motion.Y > 0) {
  127. pose = Pose.Stretching;
  128. } else if (input[0].Motion.Y < 0) {
  129. pose = Pose.Crouching;
  130. } else {
  131. pose = Pose.Standing;
  132. }
  133. DrawSightLines(collisionTargets);
  134. }
  135. Vector2 Rotate(Vector2 point, float angle) {
  136. float cos = FMath.Cos(angle);
  137. float sin = FMath.Sin(angle);
  138. return new Vector2(
  139. point.X * cos - point.Y * sin,
  140. point.Y * cos + point.X * sin);
  141. }
  142. bool PointInCone(
  143. float visionRangeSq, float fovCos, Vector2 eyePos, Vector2 direction, Vector2 test) {
  144. Vector2 delta = Vector2.Subtract(test, eyePos);
  145. if (delta.LengthSquared() > visionRangeSq) {
  146. return false;
  147. }
  148. float dot = Vector2.Dot(Vector2.Normalize(direction), Vector2.Normalize(delta));
  149. return dot > fovCos;
  150. }
  151. void DrawSightLines(AABB[] collisionTargets) {
  152. float fov = FMath.DegToRad(45);
  153. float fovCos = FMath.Cos(fov);
  154. Color color = Color.LightYellow;
  155. Vector2 eyeOffset = pose == Pose.Walking ? eyeOffsetWalking : eyeOffsetStanding;
  156. Vector2 eyePos = Vector2.Add(
  157. Position.ToVector2(), new Vector2(eyeOffset.X * (int) facing, eyeOffset.Y));
  158. float visionRange = 150;
  159. float visionRangeSq = visionRange * visionRange;
  160. Vector2 ray = new Vector2(visionRange * (int) facing, 0);
  161. if (pose == Pose.Stretching) {
  162. ray = Rotate(ray, (int) facing * FMath.DegToRad(-30));
  163. }
  164. if (pose == Pose.Crouching) {
  165. ray = Rotate(ray, (int) facing * FMath.DegToRad(30));
  166. }
  167. Vector2 coneBottom = Rotate(ray, fov);
  168. Vector2 coneTop = Rotate(ray, -fov);
  169. List<Vector2> points = new List<Vector2>();
  170. List<AABB> boxes = new List<AABB>();
  171. points.Add(Vector2.Add(eyePos, coneBottom));
  172. points.Add(Vector2.Add(eyePos, coneTop));
  173. foreach (AABB box in collisionTargets) {
  174. int hitCount = points.Count;
  175. if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.TopLeft)) {
  176. points.Add(box.TopLeft);
  177. }
  178. if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.TopRight)) {
  179. points.Add(box.TopRight);
  180. }
  181. if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.BottomLeft)) {
  182. points.Add(box.BottomLeft);
  183. }
  184. if (PointInCone(visionRangeSq, fovCos, eyePos, ray, box.BottomRight)) {
  185. points.Add(box.BottomRight);
  186. }
  187. if (points.Count > hitCount) {
  188. boxes.Add(box);
  189. Debug.AddRect(box, color);
  190. }
  191. }
  192. HashSet<AABB> boxesSeen = new HashSet<AABB>();
  193. foreach (Vector2 point in points) {
  194. float minTime = 1;
  195. AABB? closestBox = null;
  196. Vector2 delta = Vector2.Subtract(point, eyePos);
  197. foreach (AABB box in boxes) {
  198. Hit? maybeHit = box.IntersectSegment(eyePos, delta);
  199. if (maybeHit != null) {
  200. float time = FMath.Clamp(maybeHit.Value.Time, 0, 1);
  201. Vector2 target = Vector2.Add(eyePos, Vector2.Multiply(delta, time));
  202. Debug.AddLine(eyePos, target, color);
  203. if (time < minTime) {
  204. minTime = time;
  205. closestBox = box;
  206. }
  207. }
  208. }
  209. if (closestBox != null) {
  210. boxesSeen.Add(closestBox.Value);
  211. }
  212. }
  213. foreach (AABB box in boxesSeen) {
  214. Debug.AddRect(box, Color.Orange);
  215. }
  216. Debug.AddLine(eyePos, Vector2.Add(eyePos, ray), Color.Red);
  217. Debug.AddLine(eyePos, Vector2.Add(eyePos, coneTop), Color.Red);
  218. Debug.AddLine(eyePos, Vector2.Add(eyePos, coneBottom), Color.Red);
  219. }
  220. // Returns the desired (dx, dy) for the player to move this frame.
  221. Vector2 HandleInput(float modelTime, History<Input> input) {
  222. Vector2 result = new Vector2() {
  223. X = (int) (input[0].Motion.X * moveSpeed * modelTime)
  224. };
  225. if (input[0].Jump && !input[1].Jump && jumps > 0) {
  226. jumps--;
  227. ySpeed = jumpSpeed;
  228. }
  229. if (input[0].Attack && !input[1].Attack && swordSwingTime <= 0) {
  230. swordSwingTime = 0.3;
  231. swordSwingNum = (swordSwingNum + 1) % swordSwingMax;
  232. }
  233. result.Y = ySpeed * modelTime;
  234. ySpeed += gravity * modelTime;
  235. swordSwingTime -= modelTime;
  236. return result;
  237. }
  238. private int SpriteIndex(Pose pose) {
  239. int frameNum = (int) Clock.ModelTime.TotalMilliseconds / 125 % 4;
  240. switch (pose) {
  241. case Pose.Walking:
  242. return 35 + frameNum;
  243. case Pose.Jumping:
  244. return 35 + frameNum;
  245. case Pose.SwordSwing:
  246. if (swordSwingTime > 0.2) {
  247. return 0 + swordSwingNum * 3;
  248. } else if (swordSwingTime > 0.1) {
  249. return 1 + swordSwingNum * 3;
  250. } else {
  251. return 2 + swordSwingNum * 3;
  252. }
  253. case Pose.Crouching:
  254. case Pose.Stretching:
  255. case Pose.Standing:
  256. default: {
  257. if (frameNum == 3) {
  258. frameNum = 1;
  259. }
  260. return 29 + frameNum;
  261. }
  262. }
  263. }
  264. public void Draw(SpriteBatch spriteBatch) {
  265. int index = SpriteIndex(pose);
  266. Rectangle textureSource = new Rectangle(index * spriteWidth, 0, spriteWidth, spriteHeight);
  267. Vector2 spriteCenter = new Vector2(spriteWidth / 2, spriteHeight / 2 + spriteCenterYOffset);
  268. SpriteEffects effect = facing == Facing.Right ?
  269. SpriteEffects.FlipHorizontally : SpriteEffects.None;
  270. spriteBatch.Draw(texture, position.ToVector2(), textureSource, Color.White, 0f, spriteCenter,
  271. Vector2.One, effect, 0f);
  272. }
  273. }
  274. }