MonoGame 範例 NeonShooter 之 2
這是官方範例 NeonShooter 的第二篇,本篇將完成以下部分:
- 建立並發射子彈
- 子彈移動及碰撞
1. 建立並發射子彈
和建立玩家角色的方式相同,先新增一個 Bullet.cs,宣告一個 class Bullet
,接著從 class PlayerShip
複製出需要的部分包含貼圖、位置、旋轉等,還有對應的 Draw
函式。
Bullet.csusing Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace NeonShooter { public class Bullet { private Texture2D m_Image; private Vector2 m_Position = Vector2.Zero; private Color m_Color = Color.White; private float m_Rotation = 0f; private Vector2 m_Size = Vector2.Zero; private float m_Scale = 1f; public Vector2 Position { get { return m_Position; } } public float Rotation { get { return m_Rotation; } } public Vector2 Size { get { return m_Size; } } public Bullet (Texture2D _image, Vector2 _position, float _rotation) { m_Image = _image; m_Position = _position; m_Rotation = _rotation; m_Size = new Vector2 (_image.Width, _image.Height); } public void Draw (SpriteBatch _spriteBatch) { _spriteBatch.Draw (m_Image, m_Position, null, m_Color, m_Rotation, m_Size / 2f, m_Scale, SpriteEffects.None, 0f); } } }
接著要將子彈以固定的頻率生成在玩家的位置上,範例中在 class PlayerShip
中使用了一個變數 cooldowmRemaining
來控制何時生成子彈,每一次執行 Update
就會減少一次計數,當 cooldowmRemaining
等於 0 的時候就代表這次 Update
要生成子彈,之後再將 cooldowmRemaining
重新設值,達成固定頻率的目的。
我們在 class PlayerShip
新增兩個 int
變數 m_CooldownFrames
和 m_CooldownRemaining
,m_CooldownFrames
代表每幾次 Update
要發射一次子彈,會是個固定的值,因此可以宣告為 const
。
PlayerShip.cs//... private float m_Scale = 1f; private const int m_CooldownFrames = 6; private int m_CooldownRemaining = 0; public Vector2 Position { get { return m_Position; } } //...
在 Update
函式中加入對 m_CooldownRemaining
的處理,當 m_CooldownRemaining
等於 0 的時候要生成一個 Bullet
並且將值重設為 m_CooldownFrames
,最後無論有無發射子彈每次 Update
都要將 m_CooldownRemaining
減一。
PlayerShip.cspublic void Update () { //... m_Rotation = (float)Math.Atan2 (aimDirection.Y, aimDirection.X); if (m_CooldownRemaining == 0) { Bullet bullet = new Bullet (Art.Bullet, m_Position, m_Rotation); m_CooldownRemaining = m_CooldownFrames; } if (m_CooldownRemaining > 0) { m_CooldownRemaining--; } }
現在我們已經可以不斷的生成子彈了,但是沒有呼叫 Draw
函式所以無法顯示在畫面中,而且在 Update
中並不適合呼叫 Draw
,另外,生成的子彈在離開函式以後就會被解構,等於這一段都做了白工。
為了解決這個問題,可以新建一個容器用來儲存生成的 Bullet
,新增一個 BulletManager.cs,宣告一個 static class BulletManager
,因為同時會有很多子彈所以可以用 List
作為容器,宣告一個 static readonly List<Bullet>
變數 m_BulletList
。
接著新增 AddBullet
函式以便我們可以在 class PlayerShip
中將生成的子彈加入容器,最後再新增 Draw
函式將容器中的所有子彈畫到畫面上。
BulletManager.csusing Microsoft.Xna.Framework.Graphics; using System.Collections.Generic; namespace NeonShooter { public static class BulletManager { static readonly List<Bullet> m_BulletList = []; public static void AddBullet (Bullet _bullet) { m_BulletList.Add (_bullet); } public static void Draw (SpriteBatch _spriteBatch) { foreach (Bullet bullet in m_BulletList) { bullet.Draw (_spriteBatch); } } } }
在 class PlayerShip
中呼叫 AddBullet
,在 class Game1
中呼叫 Draw
。
PlayerShip.cspublic void Update () { //... if (m_CooldownRemaining == 0) { BulletManager.AddBullet (new Bullet (Art.Bullet, m_Position, m_Rotation)); m_CooldownRemaining = m_CooldownFrames; } //... }
Game1.csprotected override void Draw (GameTime _gameTime) { //... m_SpriteBatch.Begin (); m_PlayerShip.Draw (m_SpriteBatch); BulletManager.Draw (m_SpriteBatch); m_SpriteBatch.End (); //... }
生成子彈的部分已經算是完成了,現在想要一次發射兩顆子彈,而且發射的時候再加上一些偏移,讓兩顆子彈平行。
利用 Quaternion.CreateFromYawPitchRoll
可以把弧度轉換成 Quaternion
,我們是繞 Z 軸旋轉,所以只需要放入 Roll 參數,再呼叫 Vector2.Transform
把 Vector2
根據傳入的 Quaternion
做旋轉,這邊偏移量選擇 35 和 8,子彈會在角色前方距離 35 的地方出現,左右側距離為 8。
PlayerShip.cspublic void Update () { //... if (m_CooldownRemaining == 0) { // BulletManager.AddBullet (new Bullet (Art.Bullet, m_Position, m_Rotation)); Quaternion aimQuaternion = Quaternion.CreateFromYawPitchRoll (0, 0, m_Rotation); BulletManager.AddBullet (new Bullet (Art.Bullet, m_Position + Vector2.Transform (new Vector2 (35, -8), aimQuaternion), m_Rotation)); BulletManager.AddBullet (new Bullet (Art.Bullet, m_Position + Vector2.Transform (new Vector2 (35, 8), aimQuaternion), m_Rotation)); m_CooldownRemaining = m_CooldownFrames; } //... }
2. 子彈移動及碰撞
子彈生成以後會留在原地,而且隨著時間越長,畫面上的子彈會越來越多,所以接著要讓子彈開始移動,並且在離開畫面以後將子彈移除,做法也很簡單,可以想像子彈是撞到了四面牆壁所以消失了,在之後處理子彈碰到敵人時也是相同的概念。
在 class Bullet
宣告一個 Vector2
變數 m_Velocity
代表子彈速度的向量,每次 Update
時會根據這個值做位移。
Bullet.cs//... private Vector2 m_Position = Vector2.Zero; private Vector2 m_Velocity = Vector2.Zero; private Color m_Color = Color.White; //... public Bullet (Texture2D _image, Vector2 _position, Vector2 _velocity, float _rotation) { m_Image = _image; m_Position = _position; m_Velocity = _velocity; m_Rotation = _rotation; m_Size = new Vector2 (_image.Width, _image.Height); } public void Update () { m_Position += m_Velocity; }
回到 class PlayerShip
中,已知子彈的朝向是 m_Rotation
,利用 Math.Cos
和 Math.Sin
可以得到子彈朝向的單位向量,接著再乘上長度就是需要的速度了。
PlayerShip.cspublic void Update () { //... if (m_CooldownRemaining == 0) { Quaternion aimQuaternion = Quaternion.CreateFromYawPitchRoll (0, 0, m_Rotation); Vector2 velocity = 11f * new Vector2 ((float)Math.Cos (m_Rotation), (float)Math.Sin (m_Rotation)); BulletManager.AddBullet (new Bullet (Art.Bullet, m_Position + Vector2.Transform (new Vector2 (35, -8), aimQuaternion), velocity, m_Rotation)); BulletManager.AddBullet (new Bullet (Art.Bullet, m_Position + Vector2.Transform (new Vector2 (35, 8), aimQuaternion), velocity, m_Rotation)); m_CooldownRemaining = m_CooldownFrames; } //... }
在 class BulletManager
和 class Game1
中加上相關的 Update
。
BulletManager.cspublic static void Update () { foreach (Bullet bullet in m_BulletList) { bullet.Update (); } }
Game1.csprotected override void Update (GameTime _gameTime) { //... m_PlayerShip.Update (); BulletManager.Update (); //... }
如果要讓子彈超出畫面時消失,要先判斷子彈是否碰撞到畫面的邊緣,畫面的大小已經被記錄在 class Game1
的 Width
和 Height
中了,因為不需要做的太精準,只要判斷子彈的 m_Position
是否超出畫面就好了,範例中是使用 Viewport.Bounds
的 Contains
函式去判斷,如果想要將子彈的體積也納入考慮,Contains
另外也有處理 Rectangle
的版本,那就需要再計算上 m_Size
了,在這裡我們使用簡單的判斷式就可以了。
因為沒辦法在 Update
的同時移除掉子彈,宣告一個 bool
變數 m_IsExpired
,當超出畫面以後就設為 true
,在 class BulletManager
中 Update
結束時將 m_IsExpired
等於 true
的子彈從清單中移除。
Bullet.cs//... private float m_Scale = 1f; private bool m_IsExpired = false; //... public Vector2 Size { get { return m_Size; } } public bool IsExpired { get { return m_IsExpired; } } //... public void Update () { m_Position += m_Velocity; if (m_Position.X < 0 || m_Position.X > Game1.Width || m_Position.Y < 0 || m_Position.Y > Game1.Height) { m_IsExpired = true; } }
BulletManager.cspublic static void Update () { //... m_BulletList.RemoveAll (x => x.IsExpired); }