从零开发游戏需要学习的c#模块,第二十一章(精灵动画 —— 让角色走起来)

今天我们要学习的内容是

  1. 理解精灵图集的原理

  2. 加载精灵图集并切帧

  3. 实现四方向行走动画

  4. 静止时显示待机帧

第一步:准备精灵图集

精灵图集就是一张大图里包含多个小图(帧),播放时依次显示每一帧,形成动画效果。

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);
        }
    }
}

今天的学习就此结束,我叫魔法阵维护师关注我,下期更精彩!

相关推荐
Eiceblue7 小时前
使用 C# 高效替换 PDF 中的文本:全页、区域与正则匹配
visualstudio·pdf·c#
xian_wwq7 小时前
【学习笔记】探讨大模型应用安全建设系列6——合规备案:大模型备案与监管合规实操
笔记·学习·安全
xian_wwq7 小时前
【学习笔记】探讨大模型应用安全建设系列7——安全评测与红队测试
笔记·学习·安全
_李小白7 小时前
【android opencv学习笔记】Day 21: 形态学开运算与闭运算
android·opencv·学习
_李小白7 小时前
【Android车载学习笔记】第四天:AAOS系统架构
android·笔记·学习
Upsy-Daisy7 小时前
AI Agent 项目学习笔记(十):文件操作、终端执行与 PDF 生成工具
笔记·学习·pdf
nashane7 小时前
HarmonyOS 6学习:动画流畅与截图性能的双重优化实战
学习·华为·harmonyos
ゆづき7 小时前
AI能否替代小说作家?
人工智能·笔记·学习·其他·生活
_李小白7 小时前
【android opencv学习笔记】Day 20: 形态学滤波的腐蚀与膨胀
笔记·学习