今天我们要学习的内容是
-
理解精灵图集的原理
-
加载精灵图集并切帧
-
实现四方向行走动画
-
静止时显示待机帧
第一步:准备精灵图集
精灵图集就是一张大图里包含多个小图(帧),播放时依次显示每一帧,形成动画效果。
cs
一个简单的 4 帧行走图(横向排列):
[帧1][帧2][帧3][帧4]
推荐资源网站(免费):itch.io 搜 "top-down character sprite" 或 "rpg character sprite sheet"
下载任意一个角色精灵图集,放到 Content 文件夹,设为"如果较新则复制"。
第二步:创建动画管理器
右键项目 → 添加 → 类 ,文件名 Animation.cs:
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MY_FIRST_GAME
{
public class Animation
{
private Texture2D texture; // 精灵图集
private int frameCount; // 总帧数
private int currentFrame; // 当前帧
private float frameWidth; // 每帧宽度
private float frameTime; // 每帧显示时间(秒)
private float timer; // 计时器
private bool isPlaying; // 是否播放中
private bool isLooping; // 是否循环
public Animation(Texture2D texture, int frameCount, float frameTime, bool looping = true)
{
this.texture = texture;
this.frameCount = frameCount;
this.frameTime = frameTime;
this.isLooping = looping;
frameWidth = texture.Width / frameCount;
currentFrame = 0;
timer = 0f;
isPlaying = true;
}
public void Update(float deltaTime)
{
if (!isPlaying) return;
timer += deltaTime;
if (timer >= frameTime)
{
timer = 0f;
currentFrame++;
if (currentFrame >= frameCount)
{
if (isLooping)
currentFrame = 0; // 循环播放
else
{
currentFrame = frameCount - 1; // 停在最后一帧
isPlaying = false;
}
}
}
}
// 获取当前帧的源矩形
public Rectangle GetSourceRectangle()
{
return new Rectangle(
currentFrame * (int)frameWidth,
0,
(int)frameWidth,
texture.Height
);
}
public void Play()
{
isPlaying = true;
}
public void Stop()
{
isPlaying = false;
}
public void Reset()
{
currentFrame = 0;
timer = 0f;
isPlaying = true;
}
public int CurrentFrame => currentFrame;
public int FrameCount => frameCount;
public float FrameWidth => frameWidth;
public Texture2D Texture => texture;
}
}
第三步:创建玩家类
右键项目 → 添加 → 类 ,文件名 Player.cs:
cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace MY_FIRST_GAME
{
public class Player
{
public Vector2 Position { get; set; }
public float Speed { get; set; } = 200f;
public int Hp { get; set; } = 100;
public int MaxHp { get; set; } = 100;
public int Attack { get; set; } = 15;
private Texture2D texture;
private Animation? currentAnimation;
private Animation idleAnimation;
private Animation walkAnimation;
private bool isMoving;
private Vector2 lastPosition;
public Player(Texture2D spriteSheet, Vector2 startPosition)
{
Position = startPosition;
texture = spriteSheet;
// 假设精灵图集有4帧,每帧0.15秒
// 如果你的图集帧数不同,改这两个数字
idleAnimation = new Animation(texture, 1, 0f, true); // 待机用第一帧
walkAnimation = new Animation(texture, 4, 0.15f, true); // 行走4帧
currentAnimation = idleAnimation;
}
public void Update(float deltaTime)
{
KeyboardState keyboard = Keyboard.GetState();
lastPosition = Position;
isMoving = false;
if (keyboard.IsKeyDown(Keys.W) || keyboard.IsKeyDown(Keys.Up))
{ Position.Y -= Speed * deltaTime; isMoving = true; }
if (keyboard.IsKeyDown(Keys.S) || keyboard.IsKeyDown(Keys.Down))
{ Position.Y += Speed * deltaTime; isMoving = true; }
if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
{ Position.X -= Speed * deltaTime; isMoving = true; }
if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
{ Position.X += Speed * deltaTime; isMoving = true; }
// 限制边界
Position = new Vector2(
Math.Clamp(Position.X, 32, 768),
Math.Clamp(Position.Y, 32, 568)
);
// 切换动画
if (isMoving)
{
if (currentAnimation != walkAnimation)
{
walkAnimation.Reset();
currentAnimation = walkAnimation;
}
}
else
{
currentAnimation = idleAnimation;
}
currentAnimation.Update(deltaTime);
}
public void Draw(SpriteBatch spriteBatch)
{
Rectangle sourceRect = currentAnimation.GetSourceRectangle();
Vector2 origin = new Vector2(sourceRect.Width / 2, sourceRect.Height / 2);
spriteBatch.Draw(
texture,
Position,
sourceRect,
Color.White,
0f,
origin,
1f,
SpriteEffects.None,
0f
);
}
public Rectangle GetBounds()
{
Rectangle sourceRect = currentAnimation.GetSourceRectangle();
return new Rectangle(
(int)(Position.X - sourceRect.Width / 2),
(int)(Position.Y - sourceRect.Height / 2),
sourceRect.Width,
sourceRect.Height
);
}
}
}
第四步:简化版 Game1.cs
把 Game1.cs 完整替换为:
cs
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.IO;
using FontStashSharp;
namespace MY_FIRST_GAME
{
public enum GameState { Exploring, Battling }
public class Game1 : Game
{
private GraphicsDeviceManager _graphics;
private SpriteBatch _spriteBatch;
private Player player = default!;
private Texture2D playerSpriteSheet;
private Texture2D coinTexture;
private List<Vector2> coins;
private Random rng;
private int score;
private Texture2D enemyTexture;
private List<Vector2> enemies;
private SpriteFontBase font;
private GameState state = GameState.Exploring;
public Game1()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = true;
}
protected override void Initialize()
{
_graphics.PreferredBackBufferWidth = 800;
_graphics.PreferredBackBufferHeight = 600;
_graphics.ApplyChanges();
rng = new Random();
coins = new List<Vector2>();
enemies = new List<Vector2>();
score = 0;
SpawnCoins(5);
SpawnEnemies(3);
base.Initialize();
}
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
// ★ 加载精灵图集
using var stream = File.OpenRead("Content/player_spritesheet.png");
playerSpriteSheet = Texture2D.FromStream(GraphicsDevice, stream);
player = new Player(playerSpriteSheet, new Vector2(400, 300));
// 金币
coinTexture = new Texture2D(GraphicsDevice, 24, 24);
Color[] coinData = new Color[24 * 24];
for (int i = 0; i < coinData.Length; i++) coinData[i] = Color.Gold;
coinTexture.SetData(coinData);
// 敌人
enemyTexture = new Texture2D(GraphicsDevice, 40, 40);
Color[] enemyData = new Color[40 * 40];
for (int i = 0; i < enemyData.Length; i++) enemyData[i] = Color.Red;
enemyTexture.SetData(enemyData);
// 字体
var fontSystem = new FontSystem();
fontSystem.AddFont(File.ReadAllBytes("Content/consola.ttf"));
font = fontSystem.GetFont(18);
}
private void SpawnCoins(int count)
{
for (int i = 0; i < count; i++)
coins.Add(new Vector2(rng.Next(50, 750), rng.Next(50, 550)));
}
private void SpawnEnemies(int count)
{
for (int i = 0; i < count; i++)
enemies.Add(new Vector2(rng.Next(80, 720), rng.Next(80, 520)));
}
protected override void Update(GameTime gameTime)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
KeyboardState keyboard = Keyboard.GetState();
if (state == GameState.Exploring)
{
player.Update(deltaTime);
CheckCoinCollision();
CheckEnemyCollision();
if (coins.Count == 0) SpawnCoins(5);
if (enemies.Count == 0) SpawnEnemies(3);
}
if (keyboard.IsKeyDown(Keys.Escape)) Exit();
base.Update(gameTime);
}
private void CheckCoinCollision()
{
Rectangle playerRect = player.GetBounds();
for (int i = coins.Count - 1; i >= 0; i--)
{
Rectangle coinRect = new Rectangle((int)coins[i].X, (int)coins[i].Y, 24, 24);
if (playerRect.Intersects(coinRect))
{
coins.RemoveAt(i);
score += 10;
}
}
}
private void CheckEnemyCollision()
{
Rectangle playerRect = player.GetBounds();
for (int i = enemies.Count - 1; i >= 0; i--)
{
Rectangle enemyRect = new Rectangle(
(int)enemies[i].X - 20, (int)enemies[i].Y - 20, 40, 40);
if (playerRect.Intersects(enemyRect))
{
enemies.RemoveAt(i);
score += 50;
}
}
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
_spriteBatch.Begin();
// 金币
foreach (Vector2 coinPos in coins)
_spriteBatch.Draw(coinTexture, coinPos, Color.White);
// 敌人
foreach (Vector2 enemyPos in enemies)
_spriteBatch.Draw(enemyTexture, enemyPos, null, Color.White,
0f, new Vector2(20, 20), 1f, SpriteEffects.None, 0f);
// ★ 玩家(使用动画系统)
player.Draw(_spriteBatch);
// UI
_spriteBatch.DrawString(font, $"分数:{score}", new Vector2(10, 10), Color.White);
_spriteBatch.DrawString(font, $"金币:{coins.Count}", new Vector2(10, 35), Color.Gold);
_spriteBatch.DrawString(font, $"敌人:{enemies.Count}", new Vector2(10, 60), Color.Red);
_spriteBatch.DrawString(font, $"HP:{player.Hp}/{player.MaxHp}", new Vector2(10, 85), Color.LimeGreen);
_spriteBatch.DrawString(font, "WASD移动 | ESC退出", new Vector2(10, 570), Color.LightGray);
_spriteBatch.End();
base.Draw(gameTime);
}
}
}
今天的学习就此结束,我叫魔法阵维护师关注我,下期更精彩!