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>
This commit is contained in:
J. M. Ellis
2025-05-21 16:21:37 -07:00
committed by GitHub
parent 6b03d0a3f4
commit 019b35dee4
8 changed files with 494 additions and 34 deletions
+24 -12
View File
@@ -98,6 +98,30 @@
/processorParam:TextureFormat=Color
/build:player.png
#begin ro_emoji.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:ro_emoji.png
#begin Ro_Sprites/RoSpriteSheet.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:Ro_Sprites/RoSpriteSheet.png
#begin Sink area.png
/importer:TextureImporter
/processor:TextureProcessor
@@ -146,15 +170,3 @@
/processorParam:TextureFormat=Color
/build:spr_Cave1_walls.png
#begin sprRo_Wall.png
/importer:TextureImporter
/processor:TextureProcessor
/processorParam:ColorKeyColor=255,0,255,255
/processorParam:ColorKeyEnabled=True
/processorParam:GenerateMipmaps=False
/processorParam:PremultiplyAlpha=True
/processorParam:ResizeToPowerOfTwo=False
/processorParam:MakeSquare=False
/processorParam:TextureFormat=Color
/build:sprRo_Wall.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 B

+5 -3
View File
@@ -222,15 +222,17 @@ namespace LunaLightXMG
// Draw debug messages
spriteBatch.DrawString(debugFont, $"VSP: {player.vsp}", new Vector2(10, 10), Color.White);
spriteBatch.DrawString(debugFont, $"Grounded: {player.grounded}", new Vector2(10, 30), Color.White);
spriteBatch.DrawString(debugFont, $"Walled: {player.walled}", new Vector2(10, 50), Color.White);
spriteBatch.DrawString(debugFont, $"Collision Side: {player.collisionSide}", new Vector2(10, 70), Color.White);
spriteBatch.DrawString(debugFont, $"HSP: {player.hsp}", new Vector2(10, 30), Color.White);
spriteBatch.DrawString(debugFont, $"Grounded: {player.grounded}", new Vector2(10, 50), Color.White);
spriteBatch.DrawString(debugFont, $"Walled: {player.walled}", new Vector2(10, 70), Color.White);
spriteBatch.DrawString(debugFont, $"Position: {player.position}", new Vector2(10, 90), Color.White);
spriteBatch.DrawString(debugFont, $"Camera Position: {camera2D.Position}", new Vector2(10, 110), Color.White);
spriteBatch.DrawString(debugFont, $"Camera Rotation: {camera2D.Rotation}", new Vector2(10, 130), Color.White);
spriteBatch.DrawString(debugFont, $"Render Target Zoom: {renderHandler.ZoomLevel}", new Vector2(10, 150), Color.White);
spriteBatch.DrawString(debugFont, $"Player state: {player.currentState}", new Vector2(10, 170), Color.White);
spriteBatch.DrawString(debugFont, $"Camera Rotation Control - Rotate left: K, Rotate right: L", new Vector2(10, 190), Color.White);
spriteBatch.DrawString(debugFont, $"Render Target Zoom Control - Zoom in: U, Zoom normal: I, Zoom out: O", new Vector2(10, 210), Color.White);
+3 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RollForward>Major</RollForward>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
@@ -19,7 +19,8 @@
<EmbeddedResource Include="Icon.bmp" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="MonoGame.Extended" Version="4.0.4" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.2.1105" />
<PackageReference Include="MonoGame.Content.Builder.Task" Version="3.8.1.303" />
</ItemGroup>
<ItemGroup>
+259 -10
View File
@@ -1,20 +1,26 @@
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
{
private float hsp; // Horizontal speed
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?
private bool wasWalled; // Remember is the player was on a wall or ground.
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
@@ -24,12 +30,33 @@ namespace LunaLightXMG
private float grvWall; // Gravity when wall sliding
private float vspMaxWall; // Maximum vertical speed when wall sliding
private float vspJump; // Jump speed
private Texture2D texture;
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)
@@ -42,10 +69,12 @@ namespace LunaLightXMG
grounded = false;
walled = false;
wasWalled = false;
spriteFacingLeft = false;
stopped = true;
hspAcc = 0.2f;
hspFrictionGround = 0.3f;
hspFrictionAir = 0.3f;
hspWalk = 3f;
hspWalk = 2.5f;
grv = 0.2f;
vspMax = 10f;
grvWall = 0.1f;
@@ -58,31 +87,241 @@ namespace LunaLightXMG
public void LoadContent(ContentManager content)
{
texture = content.Load<Texture2D>("player");
// 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);
spriteBatch.Draw(texture, drawPosition, Color.White);
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);
}
}
// Method Implementation -------------------------------------------------------------------------------
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)
@@ -105,6 +344,14 @@ namespace LunaLightXMG
}
hsp = MathHelper.Clamp(hsp, -hspWalk, hspWalk);
if (hsp < 0)
{
spriteFacingLeft = true;
}
else if (hsp > 0)
{
spriteFacingLeft = false;
}
}
// Calculate vertical movement
@@ -157,7 +404,9 @@ namespace LunaLightXMG
collisionSide = CollisionManager.GetCollisionSide(position, boundingBox, platforms);
}
private float Approach(float start, float end, float shift)
// TODO: Move below to utilities class
private static float Approach(float start, float end, float shift)
{
if (start < end)
{
+196
View File
@@ -0,0 +1,196 @@
## March 31st, 2025 - JME
**Animated Sprites**
We are going to use the MonoGame.Extended framework for sprite animation. I have set up the project to use the extended frmework and implemented a simple test in the player class.
I learned that we should be creating sprite sheets for each character that include the animations for each state of the character, e.g. "run", "jump", "attack", etc.
The sprite sheet can be thought of as a single dimension array represented as a matrix. Each index in the array represents a frame. Lets say our run animation is 6 frames, our jump animation is 4 frames and our attack animation is 10 frames and we draw each frame in the sprite sheet whereeach frame is 18px by 18px and our sprite sheet is 90px by 72px. Then our frames can be defined as follows
[00][01][02][03][04]
[05][06][07][08][09]
[10][11][12][13][14]
[15][16][17][18][19]
Where "run" is index 00 to 05, "jump" is index 06 to 09 and "attack" is index 10 to 19.
Then from our single sprite sheet we can define the different animations for each state.
Texture2D texture2D = content.Load<Texture2D>("Path/To/SpriteSheet");
Texture2DAtlas atlas = Texture2DAtlas.Create("Atlas/SpriteSheet", texture2D, 18, 18);
SpriteSheet spriteSheet = new SpriteSheet("SpriteSheet/SpriteSheet", atlas);
spriteSheet.DefineAnimation("run", 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));
});
spriteSheet.DefineAnimation("jump", builder =>
{
builder.IsLooping(true)
.AddFrame(regionIndex: 0, duration: TimeSpan.FromSeconds(0.05))
.AddFrame(6, TimeSpan.FromSeconds(0.05))
.AddFrame(7, TimeSpan.FromSeconds(0.05))
.AddFrame(8, TimeSpan.FromSeconds(0.05))
.AddFrame(9, TimeSpan.FromSeconds(0.05));
});
spriteSheet.DefineAnimation("attack", builder =>
{
builder.IsLooping(true)
.AddFrame(regionIndex: 0, duration: 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))
.AddFrame(16, TimeSpan.FromSeconds(0.05))
.AddFrame(17, TimeSpan.FromSeconds(0.05))
.AddFrame(18, TimeSpan.FromSeconds(0.05))
.AddFrame(19, TimeSpan.FromSeconds(0.05));
});
## April 1st, 2025 - JME
**More sprite animation**
Added RoSpriteSheet containing animations for run. jump, climbLedge, climb, wallStatic, edge and idle states.
Added a drawOffset to align the 18px by 18px player sprites with the 8px by 16px bounding box.
public void Draw(SpriteBatch spriteBatch)
{
// Adjust sprite draw position to align with bounding box
Vector2 drawOffset = new Vector2(-4, -1);
// Snap position to integer values
Vector2 drawPosition = new Vector2((int)position.X, (int)position.Y) + drawOffset;
//spriteBatch.Draw(texture, drawPosition, Color.White);
spriteBatch.Draw(animatedSprite, drawPosition);
}
## April 2nd, 2025 - JME
**Handling player states and animations**
Created initial sprite animation handling. We have an UpdateAnimation method that detects player states and updates the animation based on the state.
private void UpdateAnimation()
{
if (grounded)
{
if (Math.Abs(hsp) > 0.1f)
{
HandleSetAnimation(PlayerState.Run);
}
else
{
HandleSetAnimation(PlayerState.Idle);
}
}
else if (vsp < 0)
{
HandleSetAnimation(PlayerState.Jump);
}
}
We still need logic for stop, ready, wallSlide, climbing, climgLedge, edge, attacks
We will need a timer to track idle time, as well as some other things probably.
The HandleSetAnimation method switches the animation based on state as well as other variables that might need to be set during an animation switch.
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");
break;
case PlayerState.Ready:
animatedSprite.SetAnimation("ready");
break;
case PlayerState.WallSlide:
animatedSprite.SetAnimation("wallSlide");
break;
case PlayerState.Climb:
animatedSprite.SetAnimation("climb");
break;
case PlayerState.ClimbLedge:
animatedSprite.SetAnimation("climbLedge");
break;
}
}
## April 3rd, 2025 - JME
Figured out how to flip our player sprites when moving left by creating a boolean **spriteFacingLeft** that ses to true when player.hsp is less than zero and false when player.hp is greater than zero.
Then we can create a sprite efect using SpriteEffects that changes fom FlipHorizontally to None depending on spriteFacingLeft.
The correct overload for a static sprite with a sprite effect was tricky to figure out, the best I could find is the following,
spriteBatch.Draw(wallSlide.TextureRegion.Texture,
drawPosition,
wallSlide.TextureRegion.Bounds,
Color.White,
0f,
Vector2.Zero,
1f,
flipEffect,
0f);
So for further static sprite effects we'll probably want to use this overload.
Here is the full updated draw method for the player class
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);
}
}