Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
405 lines (343 sloc) 15.6 KB
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace PlatformerStarterKit {
/// <summary>
/// Our fearless adventurer!
/// </summary>
class Player {
// Animations
private Animation idleAnimation;
private Animation runAnimation;
private Animation jumpAnimation;
private Animation celebrateAnimation;
private Animation dieAnimation;
private SpriteEffects flip = SpriteEffects.None;
private AnimationPlayer sprite;
// Sounds
private SoundEffect killedSound;
private SoundEffect jumpSound;
private SoundEffect fallSound;
public Level Level {
get { return level; }
}
Level level;
public bool IsAlive {
get { return isAlive; }
}
bool isAlive;
// Physics state
public Vector2 Position {
get { return position; }
set { position = value; }
}
Vector2 position;
private float previousBottom;
public Vector2 Velocity {
get { return velocity; }
set { velocity = value; }
}
Vector2 velocity;
#if ZUNE
// Constants for controling horizontal movement
private const float MoveAcceleration = 7000.0f;
private const float MaxMoveSpeed = 1000.0f;
private const float GroundDragFactor = 0.38f;
private const float AirDragFactor = 0.48f;
// Constants for controlling vertical movement
private const float MaxJumpTime = 0.35f;
private const float JumpLaunchVelocity = -2000.0f;
private const float GravityAcceleration = 1700.0f;
private const float MaxFallSpeed = 450.0f;
private const float JumpControlPower = 0.13f;
// Input configuration
private const float MoveStickScale = 0.0f;
private const Buttons JumpButton = Buttons.B;
#else
// Constants for controling horizontal movement
private const float MoveAcceleration = 14000.0f;
private const float MaxMoveSpeed = 2000.0f;
private const float GroundDragFactor = 0.58f;
private const float AirDragFactor = 0.65f;
// Constants for controlling vertical movement
private const float MaxJumpTime = 0.35f;
private const float JumpLaunchVelocity = -4000.0f;
private const float GravityAcceleration = 3500.0f;
private const float MaxFallSpeed = 600.0f;
private const float JumpControlPower = 0.14f;
// Input configuration
private const float MoveStickScale = 1.0f;
private const Buttons JumpButton = Buttons.A;
#endif
/// <summary>
/// Gets whether or not the player's feet are on the ground.
/// </summary>
public bool IsOnGround {
get { return isOnGround; }
}
bool isOnGround;
/// <summary>
/// Current user movement input.
/// </summary>
private float movement;
// Jumping state
private bool isJumping;
private bool wasJumping;
private float jumpTime;
private Rectangle localBounds;
/// <summary>
/// Gets a rectangle which bounds this player in world space.
/// </summary>
public Rectangle BoundingRectangle {
get {
int left = (int)Math.Round(Position.X - sprite.Origin.X) + localBounds.X;
int top = (int)Math.Round(Position.Y - sprite.Origin.Y) + localBounds.Y;
return new Rectangle(left, top, localBounds.Width, localBounds.Height);
}
}
/// <summary>
/// Constructors a new player.
/// </summary>
public Player (Level level, Vector2 position) {
this.level = level;
LoadContent();
Reset(position);
}
/// <summary>
/// Loads the player sprite sheet and sounds.
/// </summary>
public void LoadContent () {
// Load animated textures.
idleAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Idle"), 0.1f, true);
runAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Run"), 0.1f, true);
jumpAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Jump"), 0.1f, false);
celebrateAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Celebrate"), 0.1f, false);
dieAnimation = new Animation(Level.Content.Load<Texture2D>("Sprites/Player/Die"), 0.1f, false);
// Calculate bounds within texture size.
int width = (int)(idleAnimation.FrameWidth * 0.4);
int left = (idleAnimation.FrameWidth - width) / 2;
int height = (int)(idleAnimation.FrameWidth * 0.8);
int top = idleAnimation.FrameHeight - height;
localBounds = new Rectangle(left, top, width, height);
// Load sounds.
killedSound = Level.Content.Load<SoundEffect>("Sounds/PlayerKilled");
jumpSound = Level.Content.Load<SoundEffect>("Sounds/PlayerJump");
fallSound = Level.Content.Load<SoundEffect>("Sounds/PlayerFall");
}
/// <summary>
/// Resets the player to life.
/// </summary>
/// <param name="position">The position to come to life at.</param>
public void Reset (Vector2 position) {
Position = position;
Velocity = Vector2.Zero;
isAlive = true;
sprite.PlayAnimation(idleAnimation);
}
/// <summary>
/// Handles input, performs physics, and animates the player sprite.
/// </summary>
public void Update (GameTime gameTime) {
GetInput();
ApplyPhysics(gameTime);
if (IsAlive && IsOnGround) {
if (Math.Abs(Velocity.X) - 0.02f > 0) {
sprite.PlayAnimation(runAnimation);
} else {
sprite.PlayAnimation(idleAnimation);
}
}
// Clear input.
movement = 0.0f;
isJumping = false;
}
/// <summary>
/// Gets player horizontal movement and jump commands from input.
/// </summary>
private void GetInput () {
// Get input state.
GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);
KeyboardState keyboardState = Keyboard.GetState();
// Get analog horizontal movement.
movement = gamePadState.ThumbSticks.Left.X * MoveStickScale;
// Ignore small movements to prevent running in place.
if (Math.Abs(movement) < 0.5f)
movement = 0.0f;
// If any digital horizontal movement input is found, override the analog movement.
if (gamePadState.IsButtonDown(Buttons.DPadLeft) ||
keyboardState.IsKeyDown(Keys.Left) ||
keyboardState.IsKeyDown(Keys.A)) {
movement = -1.0f;
} else if (gamePadState.IsButtonDown(Buttons.DPadRight) ||
keyboardState.IsKeyDown(Keys.Right) ||
keyboardState.IsKeyDown(Keys.D)) {
movement = 1.0f;
}
// Check if the player wants to jump.
isJumping =
gamePadState.IsButtonDown(JumpButton) ||
keyboardState.IsKeyDown(Keys.Space) ||
keyboardState.IsKeyDown(Keys.Up) ||
keyboardState.IsKeyDown(Keys.W);
}
/// <summary>
/// Updates the player's velocity and position based on input, gravity, etc.
/// </summary>
public void ApplyPhysics (GameTime gameTime) {
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
Vector2 previousPosition = Position;
// Base velocity is a combination of horizontal movement control and
// acceleration downward due to gravity.
velocity.X += movement * MoveAcceleration * elapsed;
velocity.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
velocity.Y = DoJump(velocity.Y, gameTime);
// Apply pseudo-drag horizontally.
if (IsOnGround)
velocity.X *= GroundDragFactor;
else
velocity.X *= AirDragFactor;
// Prevent the player from running faster than his top speed.
velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
// Apply velocity.
Position += velocity * elapsed;
Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
// If the player is now colliding with the level, separate them.
HandleCollisions();
// If the collision stopped us from moving, reset the velocity to zero.
if (Position.X == previousPosition.X)
velocity.X = 0;
if (Position.Y == previousPosition.Y)
velocity.Y = 0;
}
/// <summary>
/// Calculates the Y velocity accounting for jumping and
/// animates accordingly.
/// </summary>
/// <remarks>
/// During the accent of a jump, the Y velocity is completely
/// overridden by a power curve. During the decent, gravity takes
/// over. The jump velocity is controlled by the jumpTime field
/// which measures time into the accent of the current jump.
/// </remarks>
/// <param name="velocityY">
/// The player's current velocity along the Y axis.
/// </param>
/// <returns>
/// A new Y velocity if beginning or continuing a jump.
/// Otherwise, the existing Y velocity.
/// </returns>
private float DoJump (float velocityY, GameTime gameTime) {
// If the player wants to jump
if (isJumping) {
// Begin or continue a jump
if ((!wasJumping && IsOnGround) || jumpTime > 0.0f) {
if (jumpTime == 0.0f)
jumpSound.Play();
jumpTime += (float)gameTime.ElapsedGameTime.TotalSeconds;
sprite.PlayAnimation(jumpAnimation);
}
// If we are in the ascent of the jump
if (0.0f < jumpTime && jumpTime <= MaxJumpTime) {
// Fully override the vertical velocity with a power curve that gives players more control over the top of the jump
velocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow(jumpTime / MaxJumpTime, JumpControlPower));
} else {
// Reached the apex of the jump
jumpTime = 0.0f;
}
} else {
// Continues not jumping or cancels a jump in progress
jumpTime = 0.0f;
}
wasJumping = isJumping;
return velocityY;
}
/// <summary>
/// Detects and resolves all collisions between the player and his neighboring
/// tiles. When a collision is detected, the player is pushed away along one
/// axis to prevent overlapping. There is some special logic for the Y axis to
/// handle platforms which behave differently depending on direction of movement.
/// </summary>
private void HandleCollisions () {
// Get the player's bounding rectangle and find neighboring tiles.
Rectangle bounds = BoundingRectangle;
int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width);
int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1;
int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height);
int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1;
// Reset flag to search for ground collision.
isOnGround = false;
// For each potentially colliding tile,
for (int y = topTile; y <= bottomTile; ++y) {
for (int x = leftTile; x <= rightTile; ++x) {
// If this tile is collidable,
TileCollision collision = Level.GetCollision(x, y);
if (collision != TileCollision.Passable) {
// Determine collision depth (with direction) and magnitude.
Rectangle tileBounds = Level.GetBounds(x, y);
Vector2 depth = RectangleExtensions.GetIntersectionDepth(bounds, tileBounds);
if (depth != Vector2.Zero) {
float absDepthX = Math.Abs(depth.X);
float absDepthY = Math.Abs(depth.Y);
// Resolve the collision along the shallow axis.
if (absDepthY < absDepthX || collision == TileCollision.Platform) {
// If we crossed the top of a tile, we are on the ground.
if (previousBottom <= tileBounds.Top)
isOnGround = true;
// Ignore platforms, unless we are on the ground.
if (collision == TileCollision.Impassable || IsOnGround) {
// Resolve the collision along the Y axis.
Position = new Vector2(Position.X, Position.Y + depth.Y);
// Perform further collisions with the new bounds.
bounds = BoundingRectangle;
}
} else if (collision == TileCollision.Impassable) // Ignore platforms.
{
// Resolve the collision along the X axis.
Position = new Vector2(Position.X + depth.X, Position.Y);
// Perform further collisions with the new bounds.
bounds = BoundingRectangle;
}
}
}
}
}
// Save the new bounds bottom.
previousBottom = bounds.Bottom;
}
/// <summary>
/// Called when the player has been killed.
/// </summary>
/// <param name="killedBy">
/// The enemy who killed the player. This parameter is null if the player was
/// not killed by an enemy (fell into a hole).
/// </param>
public void OnKilled (Enemy killedBy) {
isAlive = false;
if (killedBy != null)
killedSound.Play();
else
fallSound.Play();
sprite.PlayAnimation(dieAnimation);
}
/// <summary>
/// Called when this player reaches the level's exit.
/// </summary>
public void OnReachedExit () {
sprite.PlayAnimation(celebrateAnimation);
}
/// <summary>
/// Draws the animated player.
/// </summary>
public void Draw (GameTime gameTime, SpriteBatch spriteBatch) {
// Flip the sprite to face the way we are moving.
if (Velocity.X > 0)
flip = SpriteEffects.FlipHorizontally;
else if (Velocity.X < 0)
flip = SpriteEffects.None;
// Draw that sprite.
sprite.Draw(gameTime, spriteBatch, Position, flip);
}
}
}