Files
LunaLightXMG/Player.cs
T
J. M. Ellis 019b35dee4 Initil Sprite Animation with the player class. (#4)
* Added RoSpriteSheet and defined animation states.

* Added methods o handle sprite animation states.

* Figured out how to flip sprites when moving left.

* Bug Fix: Ready state wasn't animating, now it is ;)

---------

Co-authored-by: jme9 <jme9@pdx.edu>
2025-05-21 16:21:37 -07:00

457 lines
17 KiB
C#

using System;
using System.Reflection.Metadata;
using System.Transactions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended.Animations;
using MonoGame.Extended.Graphics;
namespace LunaLightXMG
{
public class Player
{
public float hsp; // Horizontal speed
public float vsp; // Vertical speed
private float hsp_frac; // Hold fractional position data
private float vsp_frac; // Hold fractional position data
private float wallJumpDelay; // Delay for wall jumping
public bool grounded; // Is the player grounded?
public bool walled; // Is the player wall sliding
public bool stopped; // animation control for when player has stopped moving.
private bool spriteFacingLeft;
private bool wasWalled; // Remember if the player was on a wall or ground.
private float hspAcc; // Horizontal acceleration
private float hspFrictionGround; // Ground friction
private float hspFrictionAir; // Air friction
private float hspWalk; // Maximum horizontal speed
private float grv; // Gravity
private float vspMax; // Maximum vertical speed
private float grvWall; // Gravity when wall sliding
private float vspMaxWall; // Maximum vertical speed when wall sliding
private float vspJump; // Jump speed
public Vector2 position;
public BoundingBox boundingBox { get; private set; }
public CollisionManager.CollisionSide collisionSide { get; private set; }
private CollisionManager collisionManager;
// Sprites
private Sprite wallSlide;
private AnimatedSprite animatedSprite;
public enum PlayerState
{
Idle,
Run,
Jump,
WallSlide,
Climb,
ClimbLedge,
Edge,
Stop,
Ready,
Attack1,
Attack2,
Attack3
}
public PlayerState currentState; // make private before release ;)
public Player(Vector2 initPosition)
{
hsp = 0;
vsp = 0;
hsp_frac = 0;
vsp_frac = 0;
wallJumpDelay = 0;
grounded = false;
walled = false;
wasWalled = false;
spriteFacingLeft = false;
stopped = true;
hspAcc = 0.2f;
hspFrictionGround = 0.3f;
hspFrictionAir = 0.3f;
hspWalk = 2.5f;
grv = 0.2f;
vspMax = 10f;
grvWall = 0.1f;
vspMaxWall = 5f;
vspJump = -4f;
position = initPosition;
boundingBox = new BoundingBox(new Rectangle((int)position.X, (int)position.Y, 8, 16));
collisionManager = new CollisionManager();
}
public void LoadContent(ContentManager content)
{
// Load Player SpriteSheet
Texture2D texture2D = content.Load<Texture2D>("Ro_Sprites/RoSpriteSheet");
Texture2DAtlas atlas = Texture2DAtlas.Create("Atlas/RoSpriteSheet", texture2D, 18, 18);
SpriteSheet spriteSheet = new SpriteSheet("SpriteSheet/RoSpriteSheet", atlas);
// Single Sprites
wallSlide = atlas.CreateSprite(regionIndex: 47);
// Animated Sprites
spriteSheet.DefineAnimation("climbLedge", builder =>
{
builder.IsLooping(true)
.AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.05))
.AddFrame(1, TimeSpan.FromSeconds(0.05))
.AddFrame(2, TimeSpan.FromSeconds(0.05))
.AddFrame(3, TimeSpan.FromSeconds(0.05))
.AddFrame(4, TimeSpan.FromSeconds(0.05))
.AddFrame(5, TimeSpan.FromSeconds(0.05))
.AddFrame(6, TimeSpan.FromSeconds(0.05))
.AddFrame(7, TimeSpan.FromSeconds(0.05));
});
spriteSheet.DefineAnimation("edge", builder =>
{
builder.IsLooping(true)
.AddFrame(8, TimeSpan.FromSeconds(0.05))
.AddFrame(9, TimeSpan.FromSeconds(0.05))
.AddFrame(10, TimeSpan.FromSeconds(0.05))
.AddFrame(11, TimeSpan.FromSeconds(0.05))
.AddFrame(12, TimeSpan.FromSeconds(0.05))
.AddFrame(13, TimeSpan.FromSeconds(0.05))
.AddFrame(14, TimeSpan.FromSeconds(0.05))
.AddFrame(15, TimeSpan.FromSeconds(0.05));
});
spriteSheet.DefineAnimation("jump", builder =>
{
builder.IsLooping(false)
.AddFrame(16, TimeSpan.FromSeconds(0.05))
.AddFrame(17, TimeSpan.FromSeconds(0.05))
.AddFrame(18, TimeSpan.FromSeconds(0.05))
.AddFrame(19, TimeSpan.FromSeconds(0.05))
.AddFrame(20, TimeSpan.FromSeconds(0.05))
.AddFrame(21, TimeSpan.FromSeconds(0.05))
.AddFrame(22, TimeSpan.FromSeconds(0.05))
.AddFrame(23, TimeSpan.FromSeconds(0.05));
});
spriteSheet.DefineAnimation("run", builder =>
{
builder.IsLooping(true)
.AddFrame(24, TimeSpan.FromSeconds(0.05))
.AddFrame(25, TimeSpan.FromSeconds(0.05))
.AddFrame(26, TimeSpan.FromSeconds(0.05))
.AddFrame(27, TimeSpan.FromSeconds(0.05))
.AddFrame(28, TimeSpan.FromSeconds(0.05))
.AddFrame(29, TimeSpan.FromSeconds(0.05))
.AddFrame(30, TimeSpan.FromSeconds(0.05))
.AddFrame(31, TimeSpan.FromSeconds(0.05))
.AddFrame(32, TimeSpan.FromSeconds(0.05))
.AddFrame(33, TimeSpan.FromSeconds(0.05))
.AddFrame(34, TimeSpan.FromSeconds(0.05))
.AddFrame(35, TimeSpan.FromSeconds(0.05));
});
spriteSheet.DefineAnimation("stop", builder =>
{
builder.IsLooping(false)
.AddFrame(36, TimeSpan.FromSeconds(0.05))
.AddFrame(37, TimeSpan.FromSeconds(0.05))
.AddFrame(38, TimeSpan.FromSeconds(0.05))
.AddFrame(39, TimeSpan.FromSeconds(0.05));
});
spriteSheet.DefineAnimation("ready", builder =>
{
builder.IsLooping(true)
.AddFrame(40, TimeSpan.FromSeconds(0.2))
.AddFrame(41, TimeSpan.FromSeconds(0.2))
.AddFrame(42, TimeSpan.FromSeconds(0.2))
.AddFrame(43, TimeSpan.FromSeconds(0.2))
.AddFrame(44, TimeSpan.FromSeconds(0.2))
.AddFrame(45, TimeSpan.FromSeconds(0.2))
.AddFrame(46, TimeSpan.FromSeconds(0.2));
});
spriteSheet.DefineAnimation("climb", builder =>
{
builder.IsLooping(true)
.AddFrame(48, TimeSpan.FromSeconds(0.2))
.AddFrame(49, TimeSpan.FromSeconds(0.2))
.AddFrame(50, TimeSpan.FromSeconds(0.2))
.AddFrame(51, TimeSpan.FromSeconds(0.2))
.AddFrame(52, TimeSpan.FromSeconds(0.2))
.AddFrame(53, TimeSpan.FromSeconds(0.2))
.AddFrame(54, TimeSpan.FromSeconds(0.2))
.AddFrame(55, TimeSpan.FromSeconds(0.2))
.AddFrame(56, TimeSpan.FromSeconds(0.2))
.AddFrame(57, TimeSpan.FromSeconds(0.2));
});
spriteSheet.DefineAnimation("idle", builder =>
{
builder.IsLooping(true)
.AddFrame(58, TimeSpan.FromSeconds(0.2))
.AddFrame(59, TimeSpan.FromSeconds(0.2))
.AddFrame(60, TimeSpan.FromSeconds(0.2))
.AddFrame(61, TimeSpan.FromSeconds(0.2));
});
animatedSprite = new AnimatedSprite(spriteSheet, "idle");
}
public void Update(GameTime gameTime, PlayerInput input, Platform[] platforms)
{
animatedSprite.Update(gameTime);
Movement(input);
boundingBox.Update(position, boundingBox.Bounds.Width, boundingBox.Bounds.Height);
DumpFractions();
CollisionChecks(platforms);
UpdateAnimation();
}
public void Draw(SpriteBatch spriteBatch)
{
// Adjust sprite draw position to align with bounding box
Vector2 drawOffset = new Vector2(-5, -1);
// Snap position to integer values
Vector2 drawPosition = new Vector2((int)position.X, (int)position.Y) + drawOffset;
// Flip sprites when moving left.
SpriteEffects flipEffect = spriteFacingLeft ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
// Draw sprite
if (currentState == PlayerState.WallSlide)
{
spriteBatch.Draw(wallSlide.TextureRegion.Texture,
drawPosition,
wallSlide.TextureRegion.Bounds,
Color.White,
0f,
Vector2.Zero,
1f,
flipEffect,
0f);
}
else
{
animatedSprite.Effect = flipEffect;
spriteBatch.Draw(animatedSprite, drawPosition);
}
}
public void Reset(Vector2 initialPosition)
{
position = initialPosition;
// Reset other player-specific states later
}
private void UpdateAnimation()
{
if (walled && !grounded)
{
currentState = PlayerState.WallSlide;
stopped = false;
}
else if (grounded)
{
if (hsp == 0.0f)
{
if (!stopped)
{
stopped = true;
HandleSetAnimation(PlayerState.Stop);
}
}
else
{
HandleSetAnimation(PlayerState.Run);
stopped = false;
}
}
else
{
HandleSetAnimation(PlayerState.Jump);
stopped = false;
}
// TODO: Need logic for climbing, climgLedge, edge, attacks
// TODO: Need timer to track idle time, then move to idle state after some amount of time
}
private void HandleSetAnimation(PlayerState newState)
{
if (currentState == newState) return;
currentState = newState;
switch (currentState)
{
case PlayerState.Idle:
animatedSprite.SetAnimation("idle");
break;
case PlayerState.Run:
animatedSprite.SetAnimation("run");
break;
case PlayerState.Jump:
animatedSprite.SetAnimation("jump");
break;
case PlayerState.Stop:
animatedSprite.SetAnimation("stop").OnAnimationEvent += (sender, trigger) =>
{
if (trigger == AnimationEventTrigger.AnimationCompleted)
{
HandleSetAnimation(PlayerState.Ready);
}
};
break;
case PlayerState.Ready:
animatedSprite.SetAnimation("ready");
break;
case PlayerState.WallSlide:
// not sure yet
break;
case PlayerState.Climb:
animatedSprite.SetAnimation("climb");
break;
case PlayerState.ClimbLedge:
animatedSprite.SetAnimation("climbLedge");
break;
}
}
private void Movement(PlayerInput input)
{
if (input.KeyJump && grounded)
{
vsp = vspJump;
}
// Horizontal movement with acceleration + wall jump delay
wallJumpDelay = Math.Max(wallJumpDelay - 1, 0); // counts down every frame
if (wallJumpDelay == 0) // no left-right control until delay is 0, prevents rapid wall climb
{
float moveH = input.KeyRight - input.KeyLeft;
hsp += moveH * hspAcc;
if (moveH == 0)
{
float hspFrictionFinal = grounded ? hspFrictionGround : hspFrictionAir;
hsp = Approach(hsp, 0, hspFrictionFinal);
}
hsp = MathHelper.Clamp(hsp, -hspWalk, hspWalk);
if (hsp < 0)
{
spriteFacingLeft = true;
}
else if (hsp > 0)
{
spriteFacingLeft = false;
}
}
// Calculate vertical movement
float grvFinal = grv;
float vspMaxFinal = vspMax;
if (walled && vsp > 0)
{
grvFinal = grvWall;
vspMaxFinal = vspMaxWall;
}
vsp += vsp == 0 ? grvFinal * 2 : grvFinal;
vsp = MathHelper.Clamp(vsp, vspJump, vspMaxFinal);
}
private void CollisionChecks(Platform[] platforms)
{
// Horizontal collision prediction
float onePixh = Math.Sign(hsp);
if (CollisionManager.IsColliding(position + new Vector2(hsp, 0), boundingBox, platforms))
{
while (!CollisionManager.IsColliding(position + new Vector2(onePixh, 0), boundingBox, platforms))
{
position.X += onePixh;
}
hsp = 0;
}
position.X += hsp; // Horizontal move
// Vertical collision prediction
float onePixv = Math.Sign(vsp);
if (CollisionManager.IsColliding(position + new Vector2(0, vsp), boundingBox, platforms))
{
while (!CollisionManager.IsColliding(position + new Vector2(0, onePixv), boundingBox, platforms))
{
position.Y += onePixv;
}
vsp = 0;
}
position.Y += vsp; // Vertical move
// Grounded?
grounded = CollisionManager.IsColliding(position + new Vector2(0, 1), boundingBox, platforms);
// Walled?
walled = CollisionManager.WallCheck(position, boundingBox, platforms);
if (walled)
wasWalled = walled;
collisionSide = CollisionManager.GetCollisionSide(position, boundingBox, platforms);
}
// TODO: Move below to utilities class
private static float Approach(float start, float end, float shift)
{
if (start < end)
{
start += shift;
if (start > end)
return end;
}
else
{
start -= shift;
if (start < end)
return end;
}
return start;
}
private void DumpFractions()
{
if (grounded)
{
if (collisionSide == CollisionManager.CollisionSide.Right)
{
position.X = (float)Math.Floor(position.X);
}
else if (collisionSide == CollisionManager.CollisionSide.Left)
{
position.X = (float)Math.Ceiling(position.X);
}
else
{
position.X = (float)Math.Round(position.X);
}
position.Y = (float)Math.Round(position.Y);
}
if (vsp >= 0)
{
hsp += hsp_frac;
vsp += vsp_frac;
hsp_frac = hsp - (float)Math.Floor(hsp);
vsp_frac = vsp - (float)Math.Floor(vsp);
hsp -= hsp_frac;
vsp -= vsp_frac;
}
}
}
}