mirror of
https://github.com/Memorix101/UnityXNA/
synced 2024-12-30 15:25:35 +01:00
The XNA 4.0 PlatformerGame sample is successfully running inside Unity3D 3.5. Implemented a basic game loop, game timing, content loading for Texture2D, SoundEffect and Song. Emulated SpriteBatch drawing for sprites and strings (note SpriteFont is not yet supported to all strings are rendered using the default GUI label font). Songs can be played using an AudioSource attached to the XNATest game object which acts as an emulator for MediaPlayer. Playing a SoundEffect creates a game object with an AudioSource attached which is automatically deleted when the sound finishes. Implemented keyboard input with a limited set of XNA Keys mapping to Unity3D KeyCodes.
462 lines
17 KiB
C#
462 lines
17 KiB
C#
#region File Description
|
|
//-----------------------------------------------------------------------------
|
|
// Player.cs
|
|
//
|
|
// Microsoft XNA Community Game Platform
|
|
// Copyright (C) Microsoft Corporation. All rights reserved.
|
|
//-----------------------------------------------------------------------------
|
|
#endregion
|
|
|
|
using System;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Audio;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using Microsoft.Xna.Framework.Input;
|
|
using Microsoft.Xna.Framework.Input.Touch;
|
|
|
|
namespace Platformer
|
|
{
|
|
/// <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;
|
|
|
|
// Constants for controling horizontal movement
|
|
private const float MoveAcceleration = 13000.0f;
|
|
private const float MaxMoveSpeed = 1750.0f;
|
|
private const float GroundDragFactor = 0.48f;
|
|
private const float AirDragFactor = 0.58f;
|
|
|
|
// Constants for controlling vertical movement
|
|
private const float MaxJumpTime = 0.35f;
|
|
private const float JumpLaunchVelocity = -3500.0f;
|
|
private const float GravityAcceleration = 3400.0f;
|
|
private const float MaxFallSpeed = 550.0f;
|
|
private const float JumpControlPower = 0.14f;
|
|
|
|
// Input configuration
|
|
private const float MoveStickScale = 1.0f;
|
|
private const float AccelerometerScale = 1.5f;
|
|
private const Buttons JumpButton = Buttons.A;
|
|
|
|
/// <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>
|
|
/// <remarks>
|
|
/// We pass in all of the input states so that our game is only polling the hardware
|
|
/// once per frame. We also pass the game's orientation because when using the accelerometer,
|
|
/// we need to reverse our motion when the orientation is in the LandscapeRight orientation.
|
|
/// </remarks>
|
|
public void Update(
|
|
GameTime gameTime,
|
|
KeyboardState keyboardState,
|
|
GamePadState gamePadState,
|
|
TouchCollection touchState,
|
|
AccelerometerState accelState,
|
|
DisplayOrientation orientation)
|
|
{
|
|
GetInput(keyboardState, gamePadState, touchState, accelState, orientation);
|
|
|
|
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(
|
|
KeyboardState keyboardState,
|
|
GamePadState gamePadState,
|
|
TouchCollection touchState,
|
|
AccelerometerState accelState,
|
|
DisplayOrientation orientation)
|
|
{
|
|
// 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;
|
|
|
|
// Move the player with accelerometer
|
|
if (Math.Abs(accelState.Acceleration.Y) > 0.10f)
|
|
{
|
|
// set our movement speed
|
|
movement = MathHelper.Clamp(-accelState.Acceleration.Y * AccelerometerScale, -1f, 1f);
|
|
|
|
// if we're in the LandscapeLeft orientation, we must reverse our movement
|
|
if (orientation == DisplayOrientation.LandscapeRight)
|
|
movement = -movement;
|
|
}
|
|
|
|
// 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) ||
|
|
touchState.AnyTouch();
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|