Player.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. #region File Description
  2. //-----------------------------------------------------------------------------
  3. // Player.cs
  4. //
  5. // Microsoft XNA Community Game Platform
  6. // Copyright (C) Microsoft Corporation. All rights reserved.
  7. //-----------------------------------------------------------------------------
  8. #endregion
  9. using System;
  10. using Microsoft.Xna.Framework;
  11. using Microsoft.Xna.Framework.Audio;
  12. using Microsoft.Xna.Framework.Graphics;
  13. using Microsoft.Xna.Framework.Input;
  14. using Microsoft.Xna.Framework.Input.Touch;
  15. namespace Platformer
  16. {
  17. /// <summary>
  18. /// Our fearless adventurer!
  19. /// </summary>
  20. class Player
  21. {
  22. // Animations
  23. private Animation idleAnimation;
  24. private Animation runAnimation;
  25. private Animation jumpAnimation;
  26. private Animation celebrateAnimation;
  27. private Animation dieAnimation;
  28. private SpriteEffects flip = SpriteEffects.None;
  29. private AnimationPlayer sprite;
  30. // Sounds
  31. private SoundEffect killedSound;
  32. private SoundEffect jumpSound;
  33. private SoundEffect fallSound;
  34. public Level Level
  35. {
  36. get { return level; }
  37. }
  38. Level level;
  39. public bool IsAlive
  40. {
  41. get { return isAlive; }
  42. }
  43. bool isAlive;
  44. // Physics state
  45. public Vector2 Position
  46. {
  47. get { return position; }
  48. set { position = value; }
  49. }
  50. Vector2 position;
  51. private float previousBottom;
  52. public Vector2 Velocity
  53. {
  54. get { return velocity; }
  55. set { velocity = value; }
  56. }
  57. Vector2 velocity;
  58. // Constants for controling horizontal movement
  59. private const float MoveAcceleration = 13000.0f;
  60. private const float MaxMoveSpeed = 1750.0f;
  61. private const float GroundDragFactor = 0.48f;
  62. private const float AirDragFactor = 0.58f;
  63. // Constants for controlling vertical movement
  64. private const float MaxJumpTime = 0.35f;
  65. private const float JumpLaunchVelocity = -3500.0f;
  66. private const float GravityAcceleration = 3400.0f;
  67. private const float MaxFallSpeed = 550.0f;
  68. private const float JumpControlPower = 0.14f;
  69. // Input configuration
  70. private const float MoveStickScale = 1.0f;
  71. private const float AccelerometerScale = 1.5f;
  72. private const Buttons JumpButton = Buttons.A;
  73. /// <summary>
  74. /// Gets whether or not the player's feet are on the ground.
  75. /// </summary>
  76. public bool IsOnGround
  77. {
  78. get { return isOnGround; }
  79. }
  80. bool isOnGround;
  81. /// <summary>
  82. /// Current user movement input.
  83. /// </summary>
  84. private float movement;
  85. // Jumping state
  86. private bool isJumping;
  87. private bool wasJumping;
  88. private float jumpTime;
  89. private Rectangle localBounds;
  90. /// <summary>
  91. /// Gets a rectangle which bounds this player in world space.
  92. /// </summary>
  93. public Rectangle BoundingRectangle
  94. {
  95. get
  96. {
  97. int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
  98. int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
  99. return new Rectangle(left, top, localBounds.Width, localBounds.Height);
  100. }
  101. }
  102. /// <summary>
  103. /// Constructors a new player.
  104. /// </summary>
  105. public Player(Level level, Vector2 position)
  106. {
  107. this.level = level;
  108. LoadContent();
  109. Reset(position);
  110. }
  111. /// <summary>
  112. /// Loads the player sprite sheet and sounds.
  113. /// </summary>
  114. public void LoadContent()
  115. {
  116. // Load animated textures.
  117. idleAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Idle"), 0.1f, true);
  118. runAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Run"), 0.1f, true);
  119. jumpAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Jump"), 0.1f, false);
  120. celebrateAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Celebrate"), 0.1f, false);
  121. dieAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Die"), 0.1f, false);
  122. // Calculate bounds within texture size.
  123. int width = (int)(idleAnimation.FrameWidth * 0.4);
  124. int left = (idleAnimation.FrameWidth - width) / 2;
  125. int height = (int)(idleAnimation.FrameWidth * 0.8);
  126. int top = idleAnimation.FrameHeight - height;
  127. localBounds = new Rectangle(left, top, width, height);
  128. // Load sounds.
  129. killedSound = Level.Content.Load<SoundEffect>("Sounds/PlayerKilled");
  130. jumpSound = Level.Content.Load<SoundEffect>("Sounds/PlayerJump");
  131. fallSound = Level.Content.Load<SoundEffect>("Sounds/PlayerFall");
  132. }
  133. /// <summary>
  134. /// Resets the player to life.
  135. /// </summary>
  136. /// <param name="position">The position to come to life at.</param>
  137. public void Reset(Vector2 position)
  138. {
  139. Position = position;
  140. Velocity = Vector2.Zero;
  141. isAlive = true;
  142. sprite.PlayAnimation(idleAnimation);
  143. }
  144. /// <summary>
  145. /// Handles input, performs physics, and animates the player sprite.
  146. /// </summary>
  147. /// <remarks>
  148. /// We pass in all of the input states so that our game is only polling the hardware
  149. /// once per frame. We also pass the game's orientation because when using the accelerometer,
  150. /// we need to reverse our motion when the orientation is in the LandscapeRight orientation.
  151. /// </remarks>
  152. public void Update(
  153. GameTime gameTime,
  154. KeyboardState keyboardState,
  155. GamePadState gamePadState,
  156. TouchCollection touchState,
  157. AccelerometerState accelState,
  158. DisplayOrientation orientation)
  159. {
  160. GetInput(keyboardState, gamePadState, touchState, accelState, orientation);
  161. ApplyPhysics(gameTime);
  162. if (IsAlive && IsOnGround)
  163. {
  164. if (Math.Abs(Velocity.X) - 0.02f > 0)
  165. {
  166. sprite.PlayAnimation(runAnimation);
  167. }
  168. else
  169. {
  170. sprite.PlayAnimation(idleAnimation);
  171. }
  172. }
  173. // Clear input.
  174. movement = 0.0f;
  175. isJumping = false;
  176. }
  177. /// <summary>
  178. /// Gets player horizontal movement and jump commands from input.
  179. /// </summary>
  180. private void GetInput(
  181. KeyboardState keyboardState,
  182. GamePadState gamePadState,
  183. TouchCollection touchState,
  184. AccelerometerState accelState,
  185. DisplayOrientation orientation)
  186. {
  187. // Get analog horizontal movement.
  188. movement = gamePadState.ThumbSticks.Left.X * MoveStickScale;
  189. // Ignore small movements to prevent running in place.
  190. if (Math.Abs(movement) < 0.5f)
  191. movement = 0.0f;
  192. // Move the player with accelerometer
  193. if (Math.Abs(accelState.Acceleration.Y) > 0.10f)
  194. {
  195. // set our movement speed
  196. movement = MathHelper.Clamp(-accelState.Acceleration.Y * AccelerometerScale, -1f, 1f);
  197. // if we're in the LandscapeLeft orientation, we must reverse our movement
  198. if (orientation == DisplayOrientation.LandscapeRight)
  199. movement = -movement;
  200. }
  201. // If any digital horizontal movement input is found, override the analog movement.
  202. if (gamePadState.IsButtonDown(Buttons.DPadLeft) ||
  203. keyboardState.IsKeyDown(Keys.Left) ||
  204. keyboardState.IsKeyDown(Keys.A))
  205. {
  206. movement = -1.0f;
  207. }
  208. else if (gamePadState.IsButtonDown(Buttons.DPadRight) ||
  209. keyboardState.IsKeyDown(Keys.Right) ||
  210. keyboardState.IsKeyDown(Keys.D))
  211. {
  212. movement = 1.0f;
  213. }
  214. // Check if the player wants to jump.
  215. isJumping =
  216. gamePadState.IsButtonDown(JumpButton) ||
  217. keyboardState.IsKeyDown(Keys.Space) ||
  218. keyboardState.IsKeyDown(Keys.Up) ||
  219. keyboardState.IsKeyDown(Keys.W) ||
  220. touchState.AnyTouch();
  221. }
  222. /// <summary>
  223. /// Updates the player's velocity and position based on input, gravity, etc.
  224. /// </summary>
  225. public void ApplyPhysics(GameTime gameTime)
  226. {
  227. float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
  228. Vector2 previousPosition = Position;
  229. // Base velocity is a combination of horizontal movement control and
  230. // acceleration downward due to gravity.
  231. velocity.X += movement * MoveAcceleration * elapsed;
  232. velocity.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
  233. velocity.Y = DoJump(velocity.Y, gameTime);
  234. // Apply pseudo-drag horizontally.
  235. if (IsOnGround)
  236. velocity.X *= GroundDragFactor;
  237. else
  238. velocity.X *= AirDragFactor;
  239. // Prevent the player from running faster than his top speed.
  240. velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
  241. // Apply velocity.
  242. Position += velocity * elapsed;
  243. Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
  244. // If the player is now colliding with the level, separate them.
  245. HandleCollisions();
  246. // If the collision stopped us from moving, reset the velocity to zero.
  247. if (Position.X == previousPosition.X)
  248. velocity.X = 0;
  249. if (Position.Y == previousPosition.Y)
  250. velocity.Y = 0;
  251. }
  252. /// <summary>
  253. /// Calculates the Y velocity accounting for jumping and
  254. /// animates accordingly.
  255. /// </summary>
  256. /// <remarks>
  257. /// During the accent of a jump, the Y velocity is completely
  258. /// overridden by a power curve. During the decent, gravity takes
  259. /// over. The jump velocity is controlled by the jumpTime field
  260. /// which measures time into the accent of the current jump.
  261. /// </remarks>
  262. /// <param name="velocityY">
  263. /// The player's current velocity along the Y axis.
  264. /// </param>
  265. /// <returns>
  266. /// A new Y velocity if beginning or continuing a jump.
  267. /// Otherwise, the existing Y velocity.
  268. /// </returns>
  269. private float DoJump(float velocityY, GameTime gameTime)
  270. {
  271. // If the player wants to jump
  272. if (isJumping)
  273. {
  274. // Begin or continue a jump
  275. if ((!wasJumping && IsOnGround) || jumpTime > 0.0f)
  276. {
  277. if (jumpTime == 0.0f)
  278. jumpSound.Play();
  279. jumpTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
  280. sprite.PlayAnimation(jumpAnimation);
  281. }
  282. // If we are in the ascent of the jump
  283. if (0.0f < jumpTime && jumpTime <= MaxJumpTime)
  284. {
  285. // Fully override the vertical velocity with a power curve that gives players more control over the top of the jump
  286. velocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
  287. }
  288. else
  289. {
  290. // Reached the apex of the jump
  291. jumpTime = 0.0f;
  292. }
  293. }
  294. else
  295. {
  296. // Continues not jumping or cancels a jump in progress
  297. jumpTime = 0.0f;
  298. }
  299. wasJumping = isJumping;
  300. return velocityY;
  301. }
  302. /// <summary>
  303. /// Detects and resolves all collisions between the player and his neighboring
  304. /// tiles. When a collision is detected, the player is pushed away along one
  305. /// axis to prevent overlapping. There is some special logic for the Y axis to
  306. /// handle platforms which behave differently depending on direction of movement.
  307. /// </summary>
  308. private void HandleCollisions()
  309. {
  310. // Get the player's bounding rectangle and find neighboring tiles.
  311. Rectangle bounds = BoundingRectangle;
  312. int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
  313. int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
  314. int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
  315. int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
  316. // Reset flag to search for ground collision.
  317. isOnGround = false;
  318. // For each potentially colliding tile,
  319. for (int y = topTile; y <= bottomTile; ++y)
  320. {
  321. for (int x = leftTile; x <= rightTile; ++x)
  322. {
  323. // If this tile is collidable,
  324. TileCollision collision = Level.GetCollision(x, y);
  325. if (collision != TileCollision.Passable)
  326. {
  327. // Determine collision depth (with direction) and magnitude.
  328. Rectangle tileBounds = Level.GetBounds(x, y);
  329. Vector2 depth = RectangleExtensions.GetIntersectionDepth(bounds, tileBounds);
  330. if (depth != Vector2.Zero)
  331. {
  332. float absDepthX = Math.Abs(depth.X);
  333. float absDepthY = Math.Abs(depth.Y);
  334. // Resolve the collision along the shallow axis.
  335. if (absDepthY < absDepthX || collision == TileCollision.Platform)
  336. {
  337. // If we crossed the top of a tile, we are on the ground.
  338. if (previousBottom <= tileBounds.Top)
  339. isOnGround = true;
  340. // Ignore platforms, unless we are on the ground.
  341. if (collision == TileCollision.Impassable || IsOnGround)
  342. {
  343. // Resolve the collision along the Y axis.
  344. Position = new Vector2(Position.X, Position.Y + depth.Y);
  345. // Perform further collisions with the new bounds.
  346. bounds = BoundingRectangle;
  347. }
  348. }
  349. else if (collision == TileCollision.Impassable) // Ignore platforms.
  350. {
  351. // Resolve the collision along the X axis.
  352. Position = new Vector2(Position.X + depth.X, Position.Y);
  353. // Perform further collisions with the new bounds.
  354. bounds = BoundingRectangle;
  355. }
  356. }
  357. }
  358. }
  359. }
  360. // Save the new bounds bottom.
  361. previousBottom = bounds.Bottom;
  362. }
  363. /// <summary>
  364. /// Called when the player has been killed.
  365. /// </summary>
  366. /// <param name="killedBy">
  367. /// The enemy who killed the player. This parameter is null if the player was
  368. /// not killed by an enemy (fell into a hole).
  369. /// </param>
  370. public void OnKilled(Enemy killedBy)
  371. {
  372. isAlive = false;
  373. if (killedBy != null)
  374. killedSound.Play();
  375. else
  376. fallSound.Play();
  377. sprite.PlayAnimation(dieAnimation);
  378. }
  379. /// <summary>
  380. /// Called when this player reaches the level's exit.
  381. /// </summary>
  382. public void OnReachedExit()
  383. {
  384. sprite.PlayAnimation(celebrateAnimation);
  385. }
  386. /// <summary>
  387. /// Draws the animated player.
  388. /// </summary>
  389. public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
  390. {
  391. // Flip the sprite to face the way we are moving.
  392. if (Velocity.X > 0)
  393. flip = SpriteEffects.FlipHorizontally;
  394. else if (Velocity.X < 0)
  395. flip = SpriteEffects.None;
  396. // Draw that sprite.
  397. sprite.Draw(gameTime, spriteBatch, Position, flip);
  398. }
  399. }
  400. }