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.

229 lines
7.4 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. public 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. // TODO: just make Facing an int.
  41. public int GetFacing {
  42. get { return (int) facing; }
  43. }
  44. public Pose GetPose {
  45. get { return pose; }
  46. }
  47. public Point Position { get { return position; } }
  48. public void Update(float modelTime, History<Input> input, AABB[] collisionTargets) {
  49. AABB BoxOffset(Point position, int yOffset) {
  50. return new AABB(new Vector2(position.X, position.Y + yOffset), halfSize);
  51. }
  52. AABB Box(Point position) {
  53. return BoxOffset(position, 0);
  54. }
  55. Vector2 movement = HandleInput(modelTime, input);
  56. // Broad test: remove all collision targets nowhere near the player.
  57. // TODO: don't allocate a list here.
  58. var candidates = new List<AABB>();
  59. // Expand the box in the direction of movement. The center is the midpoint of the line
  60. // between the player's current position and their desired movement. The width increases by
  61. // the magnitude of the movement in each direction. We add 1 to each dimension just to be
  62. // sure (the only downside is a small number of false-positive AABBs, which should be
  63. // discarded by later tests anyhow.)
  64. AABB largeBox = new AABB(
  65. new Vector2(position.X + movement.X / 2, position.Y + movement.Y / 2),
  66. new Vector2(halfSize.X + Math.Abs(movement.X) + 1, halfSize.Y + Math.Abs(movement.Y) + 1));
  67. foreach (var box in collisionTargets) {
  68. if (box.Intersect(largeBox) != null) {
  69. // Debug.AddRect(box, Color.Green);
  70. candidates.Add(box);
  71. }
  72. }
  73. Point[] movePoints = Line.Rasterize(0, 0, (int) movement.X, (int) movement.Y);
  74. for (int i = 1; i < movePoints.Length; i++) {
  75. int dx = movePoints[i].X - movePoints[i - 1].X;
  76. int dy = movePoints[i].Y - movePoints[i - 1].Y;
  77. if (dy != 0) {
  78. Point newPosition = new Point(position.X, position.Y + dy);
  79. AABB player = Box(newPosition);
  80. bool reject = false;
  81. foreach (var box in candidates) {
  82. if (box.Intersect(player) != null) {
  83. reject = true;
  84. break;
  85. }
  86. }
  87. if (!reject) {
  88. position = newPosition;
  89. }
  90. }
  91. if (dx != 0) {
  92. Point newPosition = new Point(position.X + dx, position.Y);
  93. AABB player = Box(newPosition);
  94. bool reject = false;
  95. foreach (var box in candidates) {
  96. if (box.Intersect(player) != null) {
  97. reject = true;
  98. break;
  99. }
  100. }
  101. if (!reject) {
  102. position = newPosition;
  103. }
  104. }
  105. }
  106. bool standingOnGround = false;
  107. AABB groundIntersect = BoxOffset(position, 1);
  108. foreach (var box in candidates) {
  109. if (groundIntersect.Intersect(box) != null) {
  110. standingOnGround = true;
  111. break;
  112. }
  113. }
  114. if (standingOnGround) {
  115. jumps = 1;
  116. ySpeed = -0.0001f;
  117. // Debug.AddRect(Box(position), Color.Cyan);
  118. } else {
  119. jumps = 0;
  120. // Debug.AddRect(Box(position), Color.Orange);
  121. }
  122. if (movement.X > 0) {
  123. facing = Facing.Right;
  124. } else if (movement.X < 0) {
  125. facing = Facing.Left;
  126. }
  127. if (swordSwingTime > 0) {
  128. pose = Pose.SwordSwing;
  129. } else if (jumps == 0) {
  130. pose = Pose.Jumping;
  131. } else if (movement.X != 0) {
  132. pose = Pose.Walking;
  133. } else if (input[0].Motion.Y > 0) {
  134. pose = Pose.Stretching;
  135. } else if (input[0].Motion.Y < 0) {
  136. pose = Pose.Crouching;
  137. } else {
  138. pose = Pose.Standing;
  139. }
  140. }
  141. public Vector2 EyePosition {
  142. get {
  143. bool walking = pose == Pose.Walking || pose == Pose.Jumping;
  144. Vector2 eyeOffset = walking ? eyeOffsetWalking : eyeOffsetStanding;
  145. return Vector2.Add(
  146. Position.ToVector2(), new Vector2(eyeOffset.X * (int) facing, eyeOffset.Y));
  147. }
  148. }
  149. // Returns the desired (dx, dy) for the player to move this frame.
  150. Vector2 HandleInput(float modelTime, History<Input> input) {
  151. Vector2 result = new Vector2() {
  152. X = (int) (input[0].Motion.X * moveSpeed * modelTime)
  153. };
  154. if (input[0].Jump && !input[1].Jump && jumps > 0) {
  155. jumps--;
  156. ySpeed = jumpSpeed;
  157. }
  158. if (input[0].Attack && !input[1].Attack && swordSwingTime <= 0) {
  159. swordSwingTime = 0.3;
  160. swordSwingNum = (swordSwingNum + 1) % swordSwingMax;
  161. }
  162. result.Y = ySpeed * modelTime;
  163. ySpeed += gravity * modelTime;
  164. swordSwingTime -= modelTime;
  165. return result;
  166. }
  167. private int SpriteIndex(Pose pose) {
  168. int frameNum = (int) Clock.ModelTime.TotalMilliseconds / 125 % 4;
  169. switch (pose) {
  170. case Pose.Walking:
  171. return 35 + frameNum;
  172. case Pose.Jumping:
  173. return 35 + frameNum;
  174. case Pose.SwordSwing:
  175. if (swordSwingTime > 0.2) {
  176. return 0 + swordSwingNum * 3;
  177. } else if (swordSwingTime > 0.1) {
  178. return 1 + swordSwingNum * 3;
  179. } else {
  180. return 2 + swordSwingNum * 3;
  181. }
  182. case Pose.Crouching:
  183. case Pose.Stretching:
  184. case Pose.Standing:
  185. default: {
  186. if (frameNum == 3) {
  187. frameNum = 1;
  188. }
  189. return 29 + frameNum;
  190. }
  191. }
  192. }
  193. public void Draw(SpriteBatch spriteBatch) {
  194. int index = SpriteIndex(pose);
  195. Rectangle textureSource = new Rectangle(index * spriteWidth, 0, spriteWidth, spriteHeight);
  196. Vector2 spriteCenter = new Vector2(spriteWidth / 2, spriteHeight / 2 + spriteCenterYOffset);
  197. SpriteEffects effect = facing == Facing.Right ?
  198. SpriteEffects.FlipHorizontally : SpriteEffects.None;
  199. spriteBatch.Draw(texture, position.ToVector2(), textureSource, Color.White, 0f, spriteCenter,
  200. Vector2.One, effect, 0f);
  201. }
  202. }
  203. }