第六章:動畫類以及動畫精靈
好久不見家人們好久沒更新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服務器,當然我會專門寫一篇來處理這些內容地。
接下來是照例地幾個問題