Player.cs 17 KB

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