第六章:动画类以及动画精灵
好久不见家人们好久没更新MonoGame系列了,不是主包弃坑了,主要是主包最近忙着搞项目学科一找暑假工打,这不一闲下来就立刻马不停蹄的来给大家更新了,今天的教程代码部分比较多接下来我们正式开始!!!
动画类:
我们知道现在许多像素独立游戏都是使用帧动画的方式来处理以及修改动画,当然了也不乏有使用骨骼动画来实现的独立游戏,但我们这个教程使用的是帧动画来实现一个最简单的动画,在学习Animation类之前我们得先学习一下什么是帧动画,在1907年逐帧动画由一位无名的技师发明,一开始作为一种实验性视频在早期的影视作品中大显风头。 起初这个技术并不被世人了解,后来,法国艾米尔科尔发现了这个独特的技术并制作了很多优秀的早期逐帧动画代表作,如逐帧定格木偶剧《小浮士德 Les Allumettes》 (1908)。 “生命之轮”(Zoerope)1867年作为玩具出现在美国,转动圆盘,透过缝隙就能看到运动形象。这个就是帧动画的前身,那么我们该如何实现这个类容呢。
第一步:创建文件并定义
我们书接上回,上次我们创建了Texture,Sprite等等的类,那么这次我们同样在Graphics文件夹下创建文件 Animation.cs
接着写入下面代码:
using System;
using System.Collections.Generic;
namespace SlimeGame.Graphics;
public class Animation
{
/// <summary>
/// 帧序列:存储所有帧图片
/// </summary>
/// <value></value>
public List<TextureRegion> Frames { get; set; }
/// <summary>
/// 帧间隔:存储每帧的时间间隔
/// </summary>
/// <value></value>
public TimeSpan Delay { get; set; }
/// <summary>
/// 无参构造
/// </summary>
public Animation()
{
Frames = new List<TextureRegion>();
Delay = TimeSpan.FromMilliseconds(100);
}
/// <summary>
/// 有参构造
/// </summary>
public Animation(List<TextureRegion> frames, TimeSpan delay)
{
Frames = frames;
Delay = delay;
}
}
第二步:修改TextureAtlas.cs使其适配Animation组件
我们回到Atlas文件的设置,我们需要修改非常多的内容接下来我们一起来修改
我们在代码中增加一个字典,用来存储所有有的动画
private Dictionary<string, Animation> Animations;
我们修改定义文件在定义中添加以下代码,为动画字典申请空间
/// <summary>
/// 无参构造
/// </summary>
public TextureAtlas()
{
Regions = new Dictionary<string, TextureRegion>();
Animations = new Dictionary<string, Animation>();
}
/// <summary>
/// 有参构造
/// </summary>
public TextureAtlas(Texture2D texture)
{
TotalTexture = texture;
Regions = new Dictionary<string, TextureRegion>();
Animations = new Dictionary<string, Animation>();
}
再然后就是我们最熟悉的增删查该环节了
/// <summary>
/// 在字典中增加动画
/// </summary>
/// <param name="animationName">动画对应名称/键</param>
/// <param name="animation">动画本体</param>
public void AddAnimation(string animationName, Animation animation)
{
Animations.Add(animationName, animation);
}
/// <summary>
/// 得到当前动画
/// </summary>
/// <param name="animationName">动画名称</param>
/// <returns>动画本体</returns>
public Animation GetAnimation(string animationName)
{
return Animations[animationName];
}
/// <summary>
/// 从字典中移除动画
/// </summary>
/// <param name="animationName">动画名称/键</param>
/// <returns>是否移除</returns>
public bool RemoveAnimation(string animationName)
{
return Animations.Remove(animationName);
}
// 清空字典
public void AnimationsClear()
{
Animations.Clear();
}
最后一步,修改文件加载方式
还记得我们上次使用XML文件加载的那个内容吗,这次我们一次性搞定,可能包括以后这部分模板部分的代码我们都不会再次修改了我们一起加油哦
/// <summary>
/// 从文件中加载纹理
/// </summary>
/// <param name="content">文件资源管理</param>
/// <param name="fileName">文件名称</param>
/// <returns></returns>
public static TextureAtlas FromFile(ContentManager content, string fileName)
{
TextureAtlas atlas = new TextureAtlas();
// 合并文件名成完整项目路径
string filePath = Path.Combine(content.RootDirectory, fileName);
//文件流式存储
using (Stream stream = TitleContainer.OpenStream(filePath))
{
// Xml读取器的获得
using (XmlReader reader = XmlReader.Create(stream))
{
// 读XMl的文件内容
XDocument doc = XDocument.Load(reader);
XElement root = doc.Root;
// <Texture> 该元素包含要加载的 Texture2D 的内容路径.
// 因此,我们将检索该值,然后使用内容管理器加载纹理.
string texturePath = root.Element("Texture").Value;
atlas.TotalTexture = content.Load<Texture2D>(texturePath);
// <Regions> 该元素包含单独的<Region>元素,每个元素描述
// 图集中的其他纹理区域.
//
// 例子:
// <Regions>
// <Region name="spriteOne" x="0" y="0" width="32" height="32" />
// <Region name="spriteTwo" x="32" y="0" width="32" height="32" />
// </Regions>
//
// 因此,我们检索所有<Region>元素,然后遍历每个元素,从中生成一个新的 TextureRegion 实例,并将其添加到此图集.
var regions = root.Element("Regions")?.Elements("Region");
if (regions != null)
{
foreach (var region in regions)
{
string name = region.Attribute("name")?.Value;
int x = int.Parse(region.Attribute("x")?.Value ?? "0");
int y = int.Parse(region.Attribute("y")?.Value ?? "0");
int width = int.Parse(region.Attribute("width")?.Value ?? "0");
int height = int.Parse(region.Attribute("height")?.Value ?? "0");
if (!string.IsNullOrEmpty(name))
{
atlas.AddRegion(name, x, y, width, height);
}
}
}
// <Animations> 该元素包含单独的<Animation>元素,每个元素描述
// 在图集中的不同动画
//
// Example:
// <Animations>
// <Animation name="animation" delay="100">
// <Frame region="spriteOne" />
// <Frame region="spriteTwo" />
// </Animation>
// </Animations>
//
// 因此,我们检索所有<Animation>元素,然后遍历每个元素
// 并从中生成新的 Animation 实例并将其添加到此图集.
var animationElements = root.Element("Animations").Elements("Animation");
if (animationElements != null)
{
foreach (var animationElement in animationElements)
{
string name = animationElement.Attribute("name")?.Value;
float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
List<TextureRegion> frames = new List<TextureRegion>();
var frameElements = animationElement.Elements("Frame");
if (frameElements != null)
{
foreach (var frameElement in frameElements)
{
string regionName = frameElement.Attribute("region").Value;
TextureRegion region = atlas.GetRegion(regionName);
frames.Add(region);
}
}
Animation animation = new Animation(frames, delay);
atlas.AddAnimation(name, animation);
}
}
return atlas;
}
}
}
至此我们Atlas的修改到此结束
第三步:创建AnimationSprite.cs
这个类是什么呢,这个就是用来在场景中渲染出这个动画的类效果,也就是每帧他会渲染出不同的Sprite而上面哪个Ainmation只起到存储的作用,Ok我们直接开始
using System;
using Microsoft.Xna.Framework;
namespace SlimeGame.Graphics;
public class AnimatedSprite : Sprite
{
/// <summary>
/// 当前帧下标
/// </summary>
private int CurrentFrame;
/// <summary>
/// 时间间隔
/// </summary>
private TimeSpan Elapsed;
/// <summary>
/// 所用动画
/// </summary>
private Animation animation;
public Animation Animation
{
get => animation;
set
{
animation = value;
Region = animation.Frames[0];
}
}
/// <summary>
/// 无参构造
/// </summary>
public AnimatedSprite() { }
/// <summary>
/// 有参构造
/// </summary>
/// <param name="animation">动画</param>
public AnimatedSprite(Animation animation)
{
Animation = animation;
}
/// <summary>
/// 循环播放当前动画组件
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
Elapsed += gameTime.ElapsedGameTime;
if (Elapsed >= animation.Delay)
{
Elapsed -= animation.Delay;
CurrentFrame++;
if (CurrentFrame >= animation.Frames.Count)
{
CurrentFrame = 0;
}
Region = animation.Frames[CurrentFrame];
}
}
}
然后我们再次返回Atlas在最后完成这个操作
public AnimatedSprite CreateAnimatedSprite(string animationName)
{
Animation animation = GetAnimation(animationName);
return new AnimatedSprite(animation);
}
那么接写下来我们所要做的工作就全部完成了那么我们接下来给出我们本章所有修改的代码:
Animation.cs
using System;
using System.Collections.Generic;
namespace SlimeGame.Graphics;
public class Animation
{
/// <summary>
/// 帧序列:存储所有帧图片
/// </summary>
/// <value></value>
public List<TextureRegion> Frames { get; set; }
/// <summary>
/// 帧间隔:存储每帧的时间间隔
/// </summary>
/// <value></value>
public TimeSpan Delay { get; set; }
/// <summary>
/// 无参构造
/// </summary>
public Animation()
{
Frames = new List<TextureRegion>();
Delay = TimeSpan.FromMilliseconds(100);
}
/// <summary>
/// 有参构造
/// </summary>
public Animation(List<TextureRegion> frames, TimeSpan delay)
{
Frames = frames;
Delay = delay;
}
}
TextureAtlas.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
namespace SlimeGame.Graphics;
/// <summary>
/// 图集类:存储纹理集和
/// </summary>
public class TextureAtlas
{
/// <summary>
/// 存储每个纹理的字典
/// </summary>
private Dictionary<string, TextureRegion> Regions;
/// <summary>
/// 存储每个动画的字典
/// </summary>
private Dictionary<string, Animation> Animations;
/// <summary>
/// 所需总体的纹理图片
/// </summary>
public Texture2D TotalTexture { get; set; }
/// <summary>
/// 无参构造
/// </summary>
public TextureAtlas()
{
Regions = new Dictionary<string, TextureRegion>();
Animations = new Dictionary<string, Animation>();
}
/// <summary>
/// 有参构造
/// </summary>
/// <param name="texture">所需总体纹理</param>
public TextureAtlas(Texture2D texture)
{
TotalTexture = texture;
Regions = new Dictionary<string, TextureRegion>();
Animations = new Dictionary<string, Animation>();
}
/// <summary>
/// 在图集里增加纹理
/// </summary>
/// <param name="name">纹理对应名称/键</param>
/// <param name="x">纹理切割X坐标</param>
/// <param name="y">纹理切割Y坐标</param>
/// <param name="width">纹理切割宽度</param>
/// <param name="height">纹理切割高度</param>
public void AddRegion(string name, int x, int y, int width, int height)
{
TextureRegion region = new TextureRegion(TotalTexture, x, y, width, height);
Regions.Add(name, region);
}
/// <summary>
/// 在字典中增加动画
/// </summary>
/// <param name="animationName">动画对应名称/键</param>
/// <param name="animation">动画本体</param>
public void AddAnimation(string animationName, Animation animation)
{
Animations.Add(animationName, animation);
}
/// <summary>
/// 从字典中查询纹理
/// </summary>
/// <param name="name">对应字典名字/键</param>
/// <returns>纹理</returns>
public TextureRegion GetRegion(string name)
{
return Regions[name];
}
/// <summary>
/// 得到当前动画
/// </summary>
/// <param name="animationName">动画名称</param>
/// <returns>动画本体</returns>
public Animation GetAnimation(string animationName)
{
return Animations[animationName];
}
/// <summary>
/// 从字典中移除纹理
/// </summary>
/// <param name="name">对应字典名字/键</param>
/// <returns>是否删除</returns>
public bool RemoveRegion(string name)
{
return Regions.Remove(name);
}
/// <summary>
/// 从字典中移除动画
/// </summary>
/// <param name="animationName">动画名称/键</param>
/// <returns>是否移除</returns>
public bool RemoveAnimation(string animationName)
{
return Animations.Remove(animationName);
}
/// <summary>
/// 清空此字典
/// </summary>
public void RegionsClear()
{
Regions.Clear();
}
public void AnimationsClear()
{
Animations.Clear();
}
/// <summary>
/// 从文件中加载纹理
/// </summary>
/// <param name="content">文件资源管理</param>
/// <param name="fileName">文件名称</param>
/// <returns></returns>
public static TextureAtlas FromFile(ContentManager content, string fileName)
{
TextureAtlas atlas = new TextureAtlas();
// 合并文件名成完整项目路径
string filePath = Path.Combine(content.RootDirectory, fileName);
//文件流式存储
using (Stream stream = TitleContainer.OpenStream(filePath))
{
// Xml读取器的获得
using (XmlReader reader = XmlReader.Create(stream))
{
// 读XMl的文件内容
XDocument doc = XDocument.Load(reader);
XElement root = doc.Root;
// <Texture> 该元素包含要加载的 Texture2D 的内容路径.
// 因此,我们将检索该值,然后使用内容管理器加载纹理.
string texturePath = root.Element("Texture").Value;
atlas.TotalTexture = content.Load<Texture2D>(texturePath);
// <Regions> 该元素包含单独的<Region>元素,每个元素描述
// 图集中的其他纹理区域.
//
// 例子:
// <Regions>
// <Region name="spriteOne" x="0" y="0" width="32" height="32" />
// <Region name="spriteTwo" x="32" y="0" width="32" height="32" />
// </Regions>
//
// 因此,我们检索所有<Region>元素,然后遍历每个元素,从中生成一个新的 TextureRegion 实例,并将其添加到此图集.
var regions = root.Element("Regions")?.Elements("Region");
if (regions != null)
{
foreach (var region in regions)
{
string name = region.Attribute("name")?.Value;
int x = int.Parse(region.Attribute("x")?.Value ?? "0");
int y = int.Parse(region.Attribute("y")?.Value ?? "0");
int width = int.Parse(region.Attribute("width")?.Value ?? "0");
int height = int.Parse(region.Attribute("height")?.Value ?? "0");
if (!string.IsNullOrEmpty(name))
{
atlas.AddRegion(name, x, y, width, height);
}
}
}
// <Animations> 该元素包含单独的<Animation>元素,每个元素描述
// 在图集中的不同动画
//
// Example:
// <Animations>
// <Animation name="animation" delay="100">
// <Frame region="spriteOne" />
// <Frame region="spriteTwo" />
// </Animation>
// </Animations>
//
// 因此,我们检索所有<Animation>元素,然后遍历每个元素
// 并从中生成新的 Animation 实例并将其添加到此图集.
var animationElements = root.Element("Animations").Elements("Animation");
if (animationElements != null)
{
foreach (var animationElement in animationElements)
{
string name = animationElement.Attribute("name")?.Value;
float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0");
TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds);
List<TextureRegion> frames = new List<TextureRegion>();
var frameElements = animationElement.Elements("Frame");
if (frameElements != null)
{
foreach (var frameElement in frameElements)
{
string regionName = frameElement.Attribute("region").Value;
TextureRegion region = atlas.GetRegion(regionName);
frames.Add(region);
}
}
Animation animation = new Animation(frames, delay);
atlas.AddAnimation(name, animation);
}
}
return atlas;
}
}
}
public Sprite CreatSprite(string regionName)
{
TextureRegion region = GetRegion(regionName);
return new Sprite(region);
}
public AnimatedSprite CreateAnimatedSprite(string animationName)
{
Animation animation = GetAnimation(animationName);
return new AnimatedSprite(animation);
}
}
AnimationSprite.cs
using System;
using Microsoft.Xna.Framework;
namespace SlimeGame.Graphics;
public class AnimatedSprite : Sprite
{
/// <summary>
/// 当前帧下标
/// </summary>
private int CurrentFrame;

/// <summary>
/// 时间间隔
/// </summary>
private TimeSpan Elapsed;
/// <summary>
/// 所用动画
/// </summary>
private Animation animation;
public Animation Animation
{
get => animation;
set
{
animation = value;
Region = animation.Frames[0];
}
}
/// <summary>
/// 无参构造
/// </summary>
public AnimatedSprite() { }
/// <summary>
/// 有参构造
/// </summary>
/// <param name="animation">动画</param>
public AnimatedSprite(Animation animation)
{
Animation = animation;
}
/// <summary>
/// 循环播放当前动画组件
/// </summary>
/// <param name="gameTime"></param>
public void Update(GameTime gameTime)
{
Elapsed += gameTime.ElapsedGameTime;
if (Elapsed >= animation.Delay)
{
Elapsed -= animation.Delay;
CurrentFrame++;
if (CurrentFrame >= animation.Frames.Count)
{
CurrentFrame = 0;
}
Region = animation.Frames[CurrentFrame];
}
}
}
第四步:使用动画组件
上面就是我们本次教程修改的全部代码了那么接下来我们来介绍如何使用这个组件:
修改·XML
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas>
<Texture>images/atlas</Texture>
<Regions>
<Region name="slime-1" x="340" y="0" width="20" height="20" />
<Region name="slime-2" x="340" y="20" width="20" height="20" />
<Region name="bat-1" x="340" y="40" width="20" height="20" />
<Region name="bat-2" x="340" y="60" width="20" height="20" />
<Region name="bat-3" x="360" y="0" width="20" height="20" />
<Region name="unfocused-button" x="259" y="80" width="65" height="14" />
<Region name="focused-button-1" x="259" y="94" width="65" height="14" />
<Region name="focused-button-2" x="259" y="109" width="65" height="14" />
<Region name="panel-background" x="324" y="97" width="15" height="15" />
<Region name="slider-off-background" x="341" y="96" width="11" height="10" />
<Region name="slider-middle-background" x="345" y="81" width="3" height="3" />
<Region name="slider-max-background" x="354" y="96" width="11" height="10" />
</Regions>
<Animations>
<Animation name="slime-animation" delay="200">
<Frame region="slime-1" />
<Frame region="slime-2" />
</Animation>
<Animation name="bat-animation" delay="200">
<Frame region="bat-1" />
<Frame region="bat-2" />
<Frame region="bat-1" />
<Frame region="bat-3" />
</Animation>
<Animation name="focused-button-animation" delay="300">
<Frame region="focused-button-1" />
<Frame region="focused-button-2" />
</Animation>
</Animations>
</TextureAtlas>
加下来将GameMain函数修改至如下所示
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using SlimeGame.Graphics;
using SlimeGameLibrary;
namespace SlimeGame;
public class GameMain : Core
{
private AnimatedSprite _slime;
private AnimatedSprite _bat;
public GameMain() : base("SlimeGame", 1280, 720, false)
{
}
protected override void Initialize()
{
// TODO: 增加你的初始化逻辑
base.Initialize();
}
protected override void LoadContent()
{
TextureAtlas atlas = TextureAtlas.FromFile(Content, "configs/atlas_slice.xml");
_slime = atlas.CreateAnimatedSprite("slime-animation");
_slime.Scale = new Vector2(4.0f, 4.0f);
_bat = atlas.CreateAnimatedSprite("bat-animation");
_bat.Scale = new Vector2(4.0f, 4.0f);
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit(); // 退出游戏:按下Esc键或是手柄上的一个啥键
// TODO: 在此处增加你的游戏主循环逻辑
_slime.Update(gameTime);
_bat.Update(gameTime);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
SpriteBatch.Begin(samplerState: SamplerState.PointClamp);
_slime.Draw(SpriteBatch, Vector2.One);
_bat.Draw(SpriteBatch, new Vector2(_slime.Width + 10, 0));
SpriteBatch.End();
base.Draw(gameTime);
}
}
老方法运行这个代码我们看看效果
结语:
最近不是打算弃坑的,只是我最近忙东西忘记更新,再加上没过几天都要学车了,我最近也是着急忙慌的看科一,当然了我这里有一个新项目给大家学习参考,也是我缺席着十几天来的成果:
https://gitee.com/qiu-tutu/eclipse-game
https://gitee.com/qiu-tutu/eclipse
这两个是我最近今天开发的项目:
线上多人射击游戏,可创建或者加入房间每个房间最多两人,由于素材不完整角色动画步完全我们接下来得不断完善整体游戏逻辑,当然是完全免费开源的,由于项目工程文件太大了,大家可以看第二个仓库的内容,里面有所有的完整代码。但是如果想要开发的话还是得自己动手学,我是用的服务器是Photon服务器,当然我会专门写一篇来处理这些内容地。
接下来是照例地几个问题