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.

215 lines
7.7 KiB

  1. using Microsoft.Xna.Framework;
  2. using Microsoft.Xna.Framework.Graphics;
  3. using Microsoft.Xna.Framework.Input;
  4. using System;
  5. using System.Collections.Generic;
  6. namespace Jumpy {
  7. class Player {
  8. enum Facing { Left, Right };
  9. enum Pose { Walking, Standing, Crouching, Stretching, SwordSwing, Jumping };
  10. enum AirState { Jumping, Ground, Falling };
  11. private Texture2D texture;
  12. private const int spriteSize = 48;
  13. private const int spriteWidth = 7;
  14. private const int moveSpeed = 180;
  15. private const int jumpSpeed = 600;
  16. private const int gravity = 2400;
  17. private Point position = new Point(64, 16);
  18. private Facing facing = Facing.Right;
  19. private Pose pose = Pose.Standing;
  20. private AirState airState = AirState.Ground;
  21. private double swordSwingTime = 0;
  22. private double jumpTime = 0;
  23. private double ySpeed = 0;
  24. public Player(Texture2D texture) {
  25. this.texture = texture;
  26. }
  27. public Point Position { get { return position; } }
  28. private Rectangle Bbox(Point position) {
  29. return new Rectangle(position.X - spriteWidth, position.Y - 7, spriteWidth * 2, 26);
  30. }
  31. public void Update(
  32. GameTime time, History<GamePadState> gamePad, History<KeyboardState> keyboard,
  33. List<Rectangle> collisionTargets) {
  34. Point oldPosition = position;
  35. AirState oldAirState = airState;
  36. UpdateFromInput(time, gamePad, keyboard);
  37. Rectangle oldBbox = Bbox(oldPosition);
  38. Rectangle playerBbox = Bbox(position);
  39. bool standingOnGround = false;
  40. // TODO: implement https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
  41. // e.g. http://members.chello.at/~easyfilter/bresenham.html
  42. // TODO: currently player doesn't fall through a gap one tile wide; presumably this will
  43. // be fixed by switching to a line-rasterization approach.
  44. foreach (var rect in collisionTargets) {
  45. playerBbox = Bbox(position);
  46. // first we check for left-right collisions...
  47. if (playerBbox.Intersects(rect)) {
  48. if (oldBbox.Right <= rect.Left && playerBbox.Right > rect.Left) {
  49. position.X = rect.Left - spriteWidth;
  50. }
  51. if (oldBbox.Left >= rect.Right && playerBbox.Left < rect.Right) {
  52. position.X = rect.Right + spriteWidth;
  53. }
  54. playerBbox = Bbox(position);
  55. }
  56. // after fixing that, we check for hitting our head or hitting the ground.
  57. if (playerBbox.Intersects(rect)) {
  58. if (oldPosition.Y > position.Y) {
  59. int diff = playerBbox.Top - rect.Bottom;
  60. position.Y -= diff;
  61. } else {
  62. airState = AirState.Ground;
  63. int diff = playerBbox.Bottom - rect.Top;
  64. position.Y -= diff;
  65. }
  66. } else {
  67. playerBbox.Height += 1;
  68. if (playerBbox.Intersects(rect)) {
  69. standingOnGround = true;
  70. Debug.AddRect(rect, Color.Cyan);
  71. } else {
  72. Debug.AddRect(rect, Color.Green);
  73. }
  74. }
  75. }
  76. if (oldAirState != AirState.Ground && standingOnGround) {
  77. airState = AirState.Ground;
  78. ySpeed = 0.0;
  79. }
  80. if (airState == AirState.Ground && !standingOnGround) {
  81. airState = AirState.Falling;
  82. ySpeed = 0.0;
  83. }
  84. if (airState == AirState.Ground) {
  85. Debug.AddRect(playerBbox, Color.Red);
  86. } else if (airState == AirState.Jumping) {
  87. Debug.AddRect(playerBbox, Color.Orange);
  88. } else {
  89. Debug.AddRect(playerBbox, Color.Yellow);
  90. }
  91. }
  92. // TODO: refactor input to have a virtual "which directions & buttons were being pressed"
  93. // instead of complicated if-statements in this function.
  94. // TODO: refactor to use a state-machine.
  95. void UpdateFromInput(
  96. GameTime time, History<GamePadState> gamePad, History<KeyboardState> keyboard) {
  97. if ((gamePad[0].IsButtonDown(Buttons.A) && gamePad[1].IsButtonUp(Buttons.A) ||
  98. keyboard[0].IsKeyDown(Keys.J) && keyboard[1].IsKeyUp(Keys.J)) &&
  99. airState == AirState.Ground) {
  100. pose = Pose.Jumping;
  101. airState = AirState.Jumping;
  102. jumpTime = 0.5;
  103. ySpeed = -jumpSpeed;
  104. return;
  105. }
  106. if ((gamePad[0].IsButtonDown(Buttons.X) && gamePad[1].IsButtonUp(Buttons.X) ||
  107. keyboard[0].IsKeyDown(Keys.K) && keyboard[1].IsKeyUp(Keys.K))
  108. && swordSwingTime <= 0) {
  109. pose = Pose.SwordSwing;
  110. swordSwingTime = 0.3;
  111. return;
  112. }
  113. Vector2 leftStick = gamePad[0].ThumbSticks.Left;
  114. // TODO: have keyboard directions cancel each other out if mutually-incompatible keys are
  115. // held down?
  116. if (gamePad[0].IsButtonDown(Buttons.DPadLeft) || leftStick.X < -0.5 ||
  117. keyboard[0].IsKeyDown(Keys.A)) {
  118. facing = Facing.Left;
  119. pose = Pose.Walking;
  120. position.X -= (int) (moveSpeed * time.ElapsedGameTime.TotalSeconds);
  121. } else if (gamePad[0].IsButtonDown(Buttons.DPadRight) || leftStick.X > 0.5 ||
  122. keyboard[0].IsKeyDown(Keys.D)) {
  123. facing = Facing.Right;
  124. pose = Pose.Walking;
  125. position.X += (int) (moveSpeed * time.ElapsedGameTime.TotalSeconds);
  126. } else if (gamePad[0].IsButtonDown(Buttons.DPadDown) || leftStick.Y < -0.5 ||
  127. keyboard[0].IsKeyDown(Keys.S)) {
  128. pose = Pose.Crouching;
  129. } else if (gamePad[0].IsButtonDown(Buttons.DPadUp) || leftStick.Y > 0.5 ||
  130. keyboard[0].IsKeyDown(Keys.W)) {
  131. pose = Pose.Stretching;
  132. } else {
  133. pose = Pose.Standing;
  134. }
  135. if (jumpTime > 0) {
  136. jumpTime -= time.ElapsedGameTime.TotalSeconds;
  137. }
  138. if (swordSwingTime > 0) {
  139. swordSwingTime -= time.ElapsedGameTime.TotalSeconds;
  140. pose = Pose.SwordSwing;
  141. }
  142. if (airState == AirState.Jumping || airState == AirState.Falling) {
  143. position.Y += (int) (ySpeed * time.ElapsedGameTime.TotalSeconds);
  144. ySpeed += gravity * (float) time.ElapsedGameTime.TotalSeconds;
  145. }
  146. if (airState == AirState.Jumping && pose != Pose.SwordSwing) {
  147. pose = Pose.Jumping;
  148. }
  149. // TODO: also bound player position by the right edge of the World?
  150. position.X = Math.Max(position.X, 0 + spriteWidth);
  151. }
  152. private int spritePosition(Pose pose, GameTime time) {
  153. int frameNum = (time.TotalGameTime.Milliseconds / 125) % 4;
  154. if (frameNum == 3) {
  155. frameNum = 1;
  156. }
  157. switch (pose) {
  158. case Pose.Walking:
  159. return 6 + frameNum;
  160. case Pose.Stretching:
  161. return 18 + frameNum;
  162. case Pose.Jumping:
  163. if (jumpTime > 0.25) {
  164. return 15;
  165. } else if (jumpTime > 0) {
  166. return 16;
  167. } else {
  168. return 17;
  169. }
  170. case Pose.SwordSwing:
  171. if (swordSwingTime > 0.2) {
  172. return 30;
  173. } else if (swordSwingTime > 0.1) {
  174. return 31;
  175. } else {
  176. return 32;
  177. }
  178. case Pose.Crouching:
  179. return 25;
  180. case Pose.Standing:
  181. default:
  182. return 7;
  183. }
  184. }
  185. public void Draw(SpriteBatch spriteBatch, Camera camera, GameTime time) {
  186. // TODO: don't create so many "new" things that could be cached / precomputed.
  187. int index = spritePosition(pose, time);
  188. Rectangle textureSource = new Rectangle(index * spriteSize, 0, spriteSize, spriteSize);
  189. Vector2 spriteCenter = new Vector2(spriteSize / 2, spriteSize / 2);
  190. SpriteEffects effect = facing == Facing.Right ?
  191. SpriteEffects.FlipHorizontally : SpriteEffects.None;
  192. Vector2 drawPos = new Vector2(position.X - camera.Left, position.Y);
  193. spriteBatch.Draw(texture, drawPos, textureSource, Color.White, 0f, spriteCenter,
  194. Vector2.One, effect, 0f);
  195. }
  196. }
  197. }