為了讓遊戲更生動,幾乎所有遊戲都會出現 2D 動畫,2D 遊戲就不用說了,3D 遊戲裡的選單和畫面狀態等也是 2D 動畫常出現的地方。
2D 動畫簡單的想法就是在短時間內變化各種不同的圖片,讓人感覺是會動的,通常我們會將相關的動作畫在同一張圖上,然後固定時間轉換顯示的位置,如下圖
先顯示最左邊的範圍,接著下一步再顯示第二個動作的範圍,依此類推,就會像動畫一樣了。整張的原圖如下:
載入整張圖片後,再藉由改變 SourceRectangle 這個參數來決定要畫哪一個部分,而為了讓動畫設定較有彈性,可以設計一些簡單的資料結構。首先是 FrameData:
public class FrameData { public Rectangle FrameRect { get; set; } public float Time { get; set; } }
由他決定要取得的矩形範圍和停留時間。再來是 AnimationData:
public class AnimationData { private Texture2D _Texture; public Texture2D Texture { get { return _Texture; } } private bool _IsLoop; public bool IsLoop { get { return _IsLoop; } } private List<FrameData> _Frames; public List<FrameData> Frames { get { return _Frames; } } public AnimationData(Texture2D texture, bool isLoop, List<FrameData> frames) { _Frames = frames; _Texture = texture; _IsLoop = isLoop; } }
一個完整的動畫會包含多個 Frame,也在此設定是否需要循環撥放。這些都是播放動畫時的一些基本資訊,再來增加一個 AnimationPlayer 物件,依據所給的資料來畫圖,程式碼如下:
public class AnimationPlayer { private int FrameIndex; private float Time; private AnimationData Data; public Vector2 Position; public Vector2 Origin; public SpriteEffects Effect; public AnimationPlayer(AnimationData data) { Data = data; FrameIndex = 0; if (Data.Frames.Count <= 0) throw new Exception("動畫無資料!"); Time = Data.Frames[0].Time; } public void Draw(float time, SpriteBatch sprite) { sprite.Draw(Data.Texture, Position, Data.Frames[FrameIndex].FrameRect, Color.White, 0, Origin, 1, Effect, 0); Time -= time; if (Time <= 0) { NextFrame(); Time = Data.Frames[FrameIndex].Time; } } private void NextFrame() { if (FrameIndex >= Data.Frames.Count - 1) { if (Data.IsLoop == true) { FrameIndex = 0; } } else FrameIndex++; } }
他的工作重點就是讓每一張圖顯示固定時間後再切換到下一張。有了這些功能後,在 LoadContent 時載入圖片和資料
protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); Texture2D animationTexture = Content.Load<Texture2D>("People"); List<FrameData> list = new List<FrameData>{ new FrameData{ FrameRect = new Rectangle(0,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(96,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(192,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(288,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(384,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(480,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(576,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(672,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(768,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(864,0,96,96), Time = 200}}; AnimationData data = new AnimationData(animationTexture, true, list); AnimationPlayer = new AnimationPlayer(data); AnimationPlayer.Position = new Vector2(100, 100); }
上面程式碼的 list 就是設定每一個 frame 的位置和顯示時間,再將圖片和 frame list 給 AnimationData。 然後是 Draw 函式
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds; // TODO: Add your drawing code here spriteBatch.Begin(); AnimationPlayer.Draw(time, spriteBatch); spriteBatch.End(); base.Draw(gameTime);
如此一來就可以在畫面上顯示跑步的動作了,藉由設定每張 frame 停留的時間,可以改變動畫的快慢。
再來設計一個腳色可以運用此動畫來移動,配合之前教的移動概念來做,腳色會有位置與速度的屬性,並且加上 AnimationPlayer 物件用來顯示動畫,以及 SpriteEffects 屬性來決定往左或往又跑。腳色物件的程式碼如下:
public class Role { private AnimationPlayer Animation; private Vector2 Origin; private SpriteEffects Direction; private Vector2 _Position; public Vector2 Position { get { return _Position; } set { _Position = value; } } private Vector2 _Velocity; public Vector2 Velocity { get { return _Velocity; } set { _Velocity = value; if (_Velocity.X < 0) Direction = SpriteEffects.None; else Direction = SpriteEffects.FlipHorizontally; } } public Role(AnimationData data) { Direction = SpriteEffects.None; Origin = new Vector2(48, 48); Animation = new AnimationPlayer(data); Animation.Origin = Origin; } public void Update(float time) { _Position += _Velocity * time; if (_Position.X + Origin.X > 800) { _Position.X = 800 - Origin.X; Velocity = Vector2.Multiply(Velocity, -Vector2.UnitX); } else if (_Position.X - Origin.X < 0) { _Position.X = Origin.X; Velocity = Vector2.Multiply(Velocity, -Vector2.UnitX); } } public void Draw(float time, SpriteBatch sprite) { Animation.Position = _Position; Animation.Effect = Direction; Animation.Draw(time, sprite); } }
在 Velocity 屬性改變時,決定面向左邊還是右邊,這裡用到的 SpriteEffects 列舉讓圖片左右鏡射,如此就不必準備兩個方向的圖了。而 Update 裡面判斷是不是超過螢幕,超過的話就轉換方向,而 Draw 的話就是把相關資訊設定給 AnimationPlayer 然後讓他畫出圖來。最後是在 game.cs 裡使用 Role 物件,相關程式碼如下:
Role Player1; protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); Texture2D animationTexture = Content.Load<Texture2D>("People"); List<FrameData> list = new List<FrameData>{ new FrameData{ FrameRect = new Rectangle(0,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(96,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(192,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(288,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(384,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(480,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(576,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(672,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(768,0,96,96), Time = 200}, new FrameData{ FrameRect = new Rectangle(864,0,96,96), Time = 200}}; AnimationData data = new AnimationData(animationTexture, true, list); Player1 = new Role(data); Player1.Position = new Vector2(100, 100); Player1.Velocity = new Vector2(0.1f, 0); } protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds; Player1.Update(time); base.Update(gameTime); } protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); float time = (float)gameTime.ElapsedGameTime.TotalMilliseconds; // TODO: Add your drawing code here spriteBatch.Begin(); Player1.Draw(time, spriteBatch); spriteBatch.End(); base.Draw(gameTime); }
由於傳入 Update 和 Draw 的時間我統一用毫秒,所以 Role 裡面的速率是像素每毫秒,這點設定時要稍微注意。而除了建構 Role 物件時必要的設定,Update 和 Draw 都只是分別呼叫相關函式即可。下面是執行畫面。他會慢慢跑。
接著來談碰撞,碰撞在大部分遊戲裡都扮演很重要的腳色,而最簡單的碰撞偵測就是直接測試矩形有沒有重疊,讓我們實作看看。
先在 Role 類別加上
private Rectangle _Bounding; public Rectangle Bounding { get { return _Bounding; } }
這是用來設定腳色會發生碰撞的範圍。然後在 Update 中不斷更新此數據
_Bounding = new Rectangle((int)(_Position.X - Origin.X),(int)( _Position.Y - Origin.Y), 96, 96);
然後在 game.cs 中再增加一個 Player2 讓他和 Player1 一起移動,在 Update 裡檢查兩個腳色是否有碰撞,有的話就彼此交換速度。
if (Player1.Bounding.Intersects(Player2.Bounding) == true) { Vector2 tempV = Player1.Velocity; Player1.Velocity = Player2.Velocity; Player2.Velocity = tempV; }
關鍵就是用 Rectangle.Intersects 來檢查兩個矩形有沒有發生重疊。以下是程式執行畫面,兩個腳色發生碰撞後會互相交換速度。