跳轉到內容

在 XNA 中建立簡單的 3D 遊戲引擎/第一章

來自華夏公益教科書,為開放世界提供開放書籍

XNA 4 引擎第一章 我假設您已經準備好 XNA 4 以供使用,如果沒有,請轉到此處以瞭解您需要什麼。我擁有本教程所需的資源,除非您已經制作了自己的太空岩石和宇宙飛船以及彈藥。您可以在這裡下載所有模型的壓縮檔案。在每個章節的末尾,我都會提供到目前為止完成的專案,供您下載,無需幫助。幫助將在頁面底部的單獨壓縮檔案中提供,我將在適當的時候解釋解壓縮的位置。您可以在此影片中看到最終的運行遊戲。它將是您完成我五章系列的第一部分的樣子,這五章將成為亞馬遜上的書籍。我們將建立和規劃我們的專案。組織極其重要。除非您喜歡撞頭,否則您會同意的。首先建立一個新的 XNA 4 Windows 遊戲專案,將其命名為 Asteroids,如圖 1 所示。

圖一

點選“確定”後,您應該看到如圖 2 所示的內容。

圖二

很好,現在我們組織專案資料夾。首先,按照圖 3 所示,在專案中新增 Entities 資料夾,然後新增 Engine 資料夾。

圖三

然後,將 Models 和 Textures 資料夾新增到專案的 Content 部分。您應該看到這兩個資料夾,如圖 4 所示。

圖四

專案資料夾應該設定為如圖 5 所示的樣子。

圖五

目前,我們只會在 Engine 資料夾中工作。即使我們直到後面的章節才會使用其他資料夾,但我喜歡提前計劃;到目前為止,這對我來說效果很好。

現在,我們將準備 Game1 類。首先,讓我們使遊戲視窗的大小合理。到目前為止,每個人都應該有一個可以處理它的螢幕,我知道您一定有一個不太舊的顯示卡,或者舊技術,否則 XNA 4 不會編譯,除非您使用 Reach 受限 API,關於這方面的內容將在後面的章節中介紹。我的引擎可以在任何一種模式下執行,Reach 模式主要啟用了一些限制器。

您應該仍然打開了 Game1.cs;在建構函式中,從清單 1.1 新增以下行。

清單 1.1 game1 編輯

Window.Title = "Asteroids 3D in XNA 4";
graphics.PreferredBackBufferWidth = 1024;
graphics.PreferredBackBufferHeight = 600;
graphics.ApplyChanges();

現在,從清單 1.2 中刪除以下行。

清單 1.2 game1

graphics = new GraphicsDeviceManager(this);

現在您應該看到如圖 6 所示的內容。

圖六

這使得它成為第一件事,它調整了任何想要玩遊戲的人都可以很好地看到它的螢幕大小,以便他們享受它。1024 x 600 只是略低於 WSVGA 的高度。這為任何至少擁有 SVGA 的玩家在視窗中留出空間,我們假設任何擁有能夠執行使用 XNA 製作的遊戲的 PC 的玩家都應該擁有 SVGA。預設的 VGA 600 x 480 對我們的目的來說太小了。在設定視窗的高度和寬度的後備緩衝區後,您必須應用更改才能使其生效。這是遊戲玻璃的一個內建方法,它按其命名執行。當您設定屬性時,它只會更改該類中的這些變數。因此,為了使這些更改生效,您必須將它們全部應用,即使您只更改了一些。到最後,這個遊戲將是解析度無關的。這只是暫時的。在後面的章節中,我們將看到如何檢視它正在執行的計算機能夠處理什麼,並使用它來設定預設螢幕尺寸。現在是開始編寫引擎類的時候了。首先,我們需要建立一個新類。將其命名為 Services;將其新增到 engine 資料夾中,如下面的圖 7 所示,右鍵單擊 Engine 資料夾,選擇 Add,然後單擊“class…”,您將看到圖 8 中的內容。

圖七
圖八

現在您應該看到如圖 9 所示的內容。

圖九

現在,我們需要新增另一個新類,Services 類將使用它,即 Camera 類,如下所示。我們需要接下來新增它,因為如果沒有它,我們就無法構建使用它的 Services 類。因此,我們將從 Camera 類開始,然後繼續處理 Services 類。它應該看起來如圖 10 所示。然後,在您點選“確定”後,您應該看到如圖 11 所示的內容。

圖十
圖十一


首先,我們需要新增一些 using 語句,我同樣喜歡對它們使用區域,如下面的清單 2.1 所示:清單 2.1 Camera.cs 新增

#region Using
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
#endregion

我刪除了 System.Collections.Generic、System.Linq 和 System.Text 名稱空間,因為它們不在此類中使用。我們需要使它成為一個公共類,因此在 class Camera 前面加上 public,如螢幕截圖所示。為了使其能夠將引擎複製到任何專案中,我們將建立名稱空間 Engine,因此將其更改為清單 2.1.1 中的樣式,清單 2.1.1 Camera.cs 編輯名稱空間

namespace Engine

接下來,我將讓您新增所有變數,或者我更喜歡稱它們為欄位。我同樣喜歡使用區域,因此我同樣讓您新增它們。按照清單 2.2 中的步驟新增類級欄位:清單 2.2 Camera.cs 新增

       #region Fields
       private Matrix cameraRotation;
       #endregion

這是對 Matrix 類的突然介紹,它是 XNA 庫的一部分,我甚至不確定它是如何工作的;對我來說,它就像一種魔法。但是,您無需瞭解它是如何工作的,即可使用它。就像您無需瞭解汽車是如何工作的,即可使用它一樣。轉到此處以瞭解更多關於 Matrix 魔法的資訊。

接下來,我將讓您新增該類所需的屬性,如下面的清單 2.3 所示。

清單 2.3 Camera.cs 新增

       #region Properties
       public Matrix View
       {
           get;
           set;
       }

       public Matrix Projection
       {
           get;
           protected set;
       }

       public Vector3 Target
       {
           get;
           set;
       }
       #endregion

您會注意到 View 和 Projection 是 Matrix 型別,而 target 是 Vector3 型別。這一點非常重要。相機的工作原理有點像投影儀,就好像它所看到的內容被投影到您的螢幕上一樣。當我們回到這個類時,我會對此進行更多解釋。

我們只需要新增最後一件東西,以便在清單 2.4 中完成它。

清單 2.4 Camera.cs 新增

       #region Constructor

       #endregion

到目前為止,您應該看到清單 2.5 和圖 12 中的內容。

清單 2.5 Camera.cs 到目前為止

#region Using
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
#endregion
namespace Engine
{
   public class Camera : PositionedObject
   {
       #region Fields
       private Matrix cameraRotation;
       #endregion

       #region Properties
       public Matrix View
       {
           get;
           set;
       }

       public Matrix Projection
       {
           get;
           protected set;
       }

       public Vector3 Target
       {
           get;
           set;
       }
       #endregion

       #region Constructor

       #endregion
   }
}
圖十二

在我們繼續之前,我們需要建立一個新類,即 Positioned Object 類。因為相機也將使用這個類,它跟蹤所有事物的位置,以及將要處於的位置。儘管這個類很小,但它非常強大。它使用遊戲計時器來跟蹤時間,因此可以應用於移動。這使得無論運行遊戲的計算機速度如何,所有事物都能平滑地移動。我對此再怎麼強調也不為過,絕不要使用遊戲迴圈來影響事物的移動速度。即使這在 80 年代使用過,當時大多數計算機的速度都相同,但這是一種不好的做法。後來的遊戲無法在較新的計算機上執行,因為它們執行得太快了。不僅如此,它還使遊戲的移動速度取決於每幀的處理速度。您肯定喜歡平滑的事物,就像我一樣。因此,我們建立 Positioned Object 類,在 engine 資料夾中名為 PositionedObject.cs。我假設您現在知道如何建立新類了。首先,我們處理 Using 名稱空間,如下面的清單 3.1 所示。

清單 3.1 PositionedObject.cs 新增

#region Using
using System;
using Microsoft.Xna.Framework;
#endregion

現在,我們需要像對 Camera 所做的那樣,將其設為公共類,在 class 前面新增 public。大多數類將是遊戲的公共類,並在冒號後面的末尾新增 DrawableGameComponent,這是繼承的類。我們使用該類的原因是,將繼承此類的遊戲物件將被繪製,draw 方法不需要在該類中。如果按照這種方式進行,遊戲中繼承此類的每個物件都將自動呼叫其 update 和 draw 方法,每幀呼叫一次。現在,有些人認為“那麼您就無法控制它們的呼叫順序”。這種假設是不正確的。您實際上可以控制順序,不僅如此,而且您擁有比平時更多的控制權。有一個用於更新順序和繪製順序的屬性,可以隨時更改它們。預設情況下,順序是按照例項化的順序進行的。同樣,如果您不想每幀都呼叫 Draw 方法,請使用 Visible 屬性,如果您不想每幀都呼叫 Update 方法,請使用 Enabled 屬性。就這麼簡單。僅僅因為您不知道如何使用某些東西,並不意味著它無法做到。這是庫中內建的原因,應該使用所有可用的工具。我打算糾正這種謬誤。在我們完成之前,您會親吻這些 Component 類,如果您像我一樣的話。

我們需要在這個類上更改名稱空間,就像在清單 3.1.1 中一樣。

清單 3.1.1 PositionedObject.cs 編輯名稱空間

namespace Engine

現在類行應該看起來像清單 3.2 中的樣式。

清單 3.2 PositionedObject.cs 新增

public abstract class PositionedObject : DrawableGameComponent

這使得一個類只能被繼承,而不能被例項化。這應該作為最佳實踐在所有基類上進行。

現在,我們新增您在清單 3.2.5 中看到的欄位。

清單 3.2.5 PositionedObject.cs 新增

       #region Fields
       private float frameTime;
       // Doing these as fields is almost twice as fast as if they were properties.
       // Also, sense XYZ are fields they do not get data binned as a property.
       public Vector3 Position;
       public Vector3 Acceleration;
       public Vector3 Velocity;
       public Vector3 RotationInRadians;
       public Vector3 ScalePercent;
       public Vector3 RotationVelocity;
       public Vector3 RotationAcceleration;
       #endregion

現在,您可能想知道為什麼我們不為所有這些使用屬性,事實證明,使用屬性比使用欄位要慢。這是 Vector3 不使用屬性的相同原因。此類每幀執行一次,因此速度極其重要,每毫秒都算,因為螢幕上的每個物件都將使用此類。

現在,我希望您新增如下面的清單 3.3 所示的建構函式。

清單 3.3 PositionedObject.cs 新增

       #region Constructor
       /// <summary>
       /// This gets the Positioned Object ready for use, initializing all the fields.
       /// </summary>
       /// <param name="game">The game class</param>
       public PositionedObject(Game game)
           : base(game)
       {
          
       }
       #endregion

這裡我們只需要將遊戲類傳遞給元件類。

接下來是執行所有工作的那個方法。請注意 override,如果您已經瞭解它的作用,請繼續;如果您不瞭解,請繼續閱讀。已經有一個相同名稱的方法,它是一個虛方法,我們重寫它。當該方法被呼叫時,它會一直向上追溯到這個類,並由元件類自動呼叫;然後,我們向下追溯到基類以呼叫它。我們將在此處停止,並稍後回來。update 方法使用一個需要在 Services 類中建立的方法。現在它應該看起來像清單 3.5 中的那樣。

清單 3.5 PositionedObject.cs 到目前為止

#region Using
using System;
using Microsoft.Xna.Framework;
#endregion

namespace Engine
{
   public class PositionedObject : DrawableGameComponent
   {
       #region Fields
       private float frameTime;
       // Doing these as fields is almost twice as fast as if they were properties.
       // Also, sense XYZ are fields they do not get data binned as a property.
       public Vector3 Position;
       public Vector3 Acceleration;
       public Vector3 Velocity;
       public Vector3 RotationInRadians;
       public Vector3 ScalePercent;
       public Vector3 RotationVelocity;
       public Vector3 RotationAcceleration;

       #endregion

       #region Constructor
       /// <summary>
       /// This gets the Positioned Object ready for use, initializing all the fields.
       /// </summary>
       /// <param name="game">The game class</param>
       public PositionedObject(Game game)
           : base(game)
       {
           game.Components.Add(this);
       }
       #endregion

       #region Public Methods

       #endregion
   }
}

注意圖 13 中我仍然打開了所有類。我們將來會用到它們,所以您可能也希望將它們保持開啟狀態,以便您可以輕鬆地在它們之間切換選項卡。

圖十三

現在我們回到 Services 類,開始處理它。您可能會發現自己經常這樣做,在不同的類之間切換,因為它們相互依賴,並且您應該等到建立了計劃新增的部分後再新增它們。現在,我將首先讓您更改類行,這是一個特殊的類,稱為單例類。為了確保它無法被呼叫或初始化,我們將使用 sealed 關鍵字,如清單 4.1 中所示更改該行。

清單 4.1 Services.cs 編輯 public sealed class Services 我們還需要像清單 4.1.1 中那樣更改此類中的名稱空間。清單 4.1.1 Services.cs 編輯名稱空間 namespace Engine 接下來,我們按照清單 4.2 中的步驟新增欄位:清單 4.2 Services.cs 新增

       #region Fields
       private static Services instance = null;
       private static GraphicsDevice graphics;
       private static Random randomNumber;
       #endregion

我們將確保此類在生命週期中只能存在一次,因此我們有 Services 例項行。稍後會檢查它以確保它只能有一個例項。只能有一個!這裡有我們的類將提供訪問許可權的服務,即相機、圖形和隨機數生成器。圖形就是圖形,您看到的所有內容。這裡我們有一個隨機數生成器,您必須確保只使用一個,還有什麼比將它放在此類中更好的方法呢?

現在,我將讓您新增所有屬性,並且有幾個。按照清單 4.3 中的步驟新增它們。

清單 4.3 Services.cs 新增

       #region Properties
       /// <summary>
       /// This is used to get the Services Instance
       /// Instead of using the mInstance this will do the check to see if the Instance is valid
       /// where ever you use it. It is also private so it will only get used inside the engine services.
       /// </summary>
       private static Services Instance
       {
           get
           {
               //Make sure the Instance is valid
               if (instance != null)
               {
                   return instance;
               }

               throw new InvalidOperationException("The Engine Services have not been started!");
           }
       }

       public static Camera Camera
       {
           get { return camera; }
       }

       public static GraphicsDevice Graphics
       {
           get { return graphics; }
       }

       public static Random RandomNumber
       {
           get { return randomNumber; }
       } 
       /// <summary>
       /// Returns elapsed seconds, in milliseconds.
       /// </summary>
       /// <returns>double</returns>
       /// <summary>
       /// Returns the window size in pixels, of the height.
       /// </summary>
       /// <returns>int</returns>
       public static int WindowHeight
       {
           get { return graphics.ScissorRectangle.Height; }
       }

       /// <summary>
       /// Returns the window size in pixels, of the width.
       /// </summary>
       /// <returns>int</returns>
       public static int WindowWidth
       {
           get { return graphics.ScissorRectangle.Width; }
       }
       #endregion

我希望您不會覺得一次新增所有這些內容讓人不知所措。這裡有很多事情要做,我們將在稍後使用它們。請記住,提前計劃!如您所見,第一個是例項檢查器,我在摘要中對此進行了解釋。接下來是相機、圖形、遊戲時間和隨機數生成器的訪問屬性。然後,我們具有用於訪問視窗高度和寬度的屬性。請注意,Camera 具有私有 set 訪問器,這是因為我們在初始化時從 Game 類中傳入相機引用,如您將在到達該方法時看到的那樣。這是我們唯一允許訪問以設定主相機的相機引用的時間。

接下來,我們按照清單 4.4 中的步驟新增建構函式。

清單 4.4 Services.cs 新增

       #region Constructor
               /// <summary>
       /// This is the constructor for the Services
       /// You will note that it is private that means that only the Services can create itself.
       /// </summary>
       private Services(Game game)
       {
          
       }
       #endregion

您會注意到它是私有的;這使得您無法在自身之外對其進行例項化。您會問,為什麼要這樣做?這就是您建立單例類的方式。請記住,只能有一個。目前,我們將在此類上進行的所有工作就是這些。在我們進一步研究之前,我們需要再次回到 Camera 類。

這是您目前應該具有的內容,如清單 4.5 所示。

清單 4.5 Services.cs 目前

#region Using
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
#endregion

namespace Engine
{
   public sealed class Services
   {
       #region Fields
       private static Services instance = null;
       private static GraphicsDevice graphics;
       private static Random randomNumber;
       #endregion 
       #region Properties
       /// <summary>
       /// This is used to get the Services Instance
       /// Instead of using the mInstance this will do the check to see if the Instance is valid
       /// where ever you use it. It is also private so it will only get used inside the engine services.
       /// </summary>
       private static Services Instance
       {
           get
           {
               //Make sure the Instance is valid
               if (instance != null)
               {
                   return instance;
               }

               throw new InvalidOperationException("The Engine Services have not been started!");
           }
       }

       public static Camera Camera
       {
           get;
           private set;
       }

       public static GraphicsDevice Graphics
       {
           get { return graphics; }
       }

       public static Random RandomNumber
       {
           get { return randomNumber; }
       } 
       /// <summary>
       /// Returns elapsed seconds, in milliseconds.
       /// </summary>
       /// <returns>double</returns>
       /// <summary>
       /// Returns the window size in pixels, of the height.
       /// </summary>
       /// <returns>int</returns>
       public static int WindowHeight
       {
           get { return graphics.ScissorRectangle.Height; }
       }

       /// <summary>
       /// Returns the window size in pixels, of the width.
       /// </summary>
       /// <returns>int</returns>
       public static int WindowWidth
       {
           get { return graphics.ScissorRectangle.Width; }
       }
       #endregion

       #region Constructor
               /// <summary>
       /// This is the constructor for the Services
       /// You will note that it is private that means that only the Services can only create itself.
       /// </summary>
       private Services(Game game)
       {
          
       }
       #endregion

       #region Public Methods

       #endregion
   }
}

好吧,您知道我會這樣做的,回到 Camera 類!我希望在本章結束時,您將能夠理解為什麼我一直這樣來回切換。您可能需要像我一樣,喝很多咖啡。在 Camera 類準備進行編輯後,我們將新增它將繼承的第一個類。因此,按照 5.1 中的步驟更改類行。

清單 5.1 Camera.cs 編輯

public class Camera : PositionedObject

我們還需要像清單 5.1.1 中那樣更改此類中的名稱空間。

清單 5.1.1 Camera.cs 編輯名稱空間

namespace Engine

現在我們可以新增建構函式,並且此建構函式做了很多事情,因此請仔細閱讀清單 5.2 中的步驟。

清單 5.2 Camera.cs 編輯

       #region Constructor
       public Camera(Game game, Vector3 position, Vector3 target, Vector3 rotation,
bool Orthographic, float near, float far)
           : base(game)
       {
           Position = position;
           RotationInRadians = rotation;
           Target = target;

           if (Orthographic)
           {
               Projection = Matrix.CreateOrthographic(Game.Window.ClientBounds.Width, 
Game.Window.ClientBounds.Height,
                   near, far);
           }
           else
           {
               Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
                   (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height,
 near, far);
           }
       }
       #endregion

第一行非常重要,您會看到它呼叫了遊戲元件集合。這就是 Game Component 如何跟蹤要更新哪個類以及何時更新。預設情況下,它們按新增順序進行更新。因此,每個屬於 Positioned Object 或 Game Component 的類都需要像這樣新增,否則它將不會被更新。這是您唯一需要注意的事情。我會讓您輕鬆完成,當我們到達那裡時,我會讓您建立一個通用敵人類,除了玩家之外的所有敵人都會繼承它,它將包含該行,因此您無需記住每次都將其放在那裡。只有玩家以及任何不是獨立類的物件的類需要它。

現在您可以看到矩陣是多麼神奇。現在我們有了 Positioned Object 類,我們可以從它繼承,因此我們可以使用它來跟蹤運動及其正確位置。由於相機的運作方式,我們必須將位置轉換為它理解的東西。我們使用庫中內建的方法來實現這一點!我們這裡還使用了一個 switch 語句,以防我們想要將其用於正交相機。這將用於選單、HUD 或使用精靈的 2D 遊戲。這是一個 3D 引擎,因此預設相機不是正交相機。當我們回到 Services 類時,您會看到這一點。其餘部分只是傳遞給 Position 和 Rotation,並且我們之前添加了 Target Vector3。當您例項化另一個相機時,您將告訴它該怎麼做,所有這些都是傳入的,如您所見。此外,您會看到 near 和 far float 型別欄位,它告訴它渲染物件的距離範圍。所有這些都在矩陣中計算,如您所見。現在我們繼續進行公共方法,第一個是 Initialize 方法,如清單 5.3 中所示。

清單 5.3 Camera.cs 編輯

       #region Public Methods
       /// <summary>
       /// Allows the game component to perform any initialization it needs to before starting
       /// to run.  This is where it can query for any required services and load content.
       /// </summary>
       public override void Initialize()
       {
           base.Initialize();
           cameraRotation = Matrix.Identity;
       }       
       #endregion

Matrix.Identity 屬性是一個空白但非空的矩陣,就像建立一個值為 0 的新 int 一樣,例如 int i = 0;因此您會看到,它並沒有做太多事情,但它很方便。我希望您還記得我們在另一個類中之前執行的相同方法中的其餘部分。接下來我們有 Update 方法,在清單 5.4 中看到的相同區域內新增如下內容。

清單 5.4 Camera.cs 編輯

       /// <summary>
       /// Allows the game component to update itself via the GameComponent.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       public override void Update(GameTime gameTime)
       {
           base.Update(gameTime);
           // This rotates the free camera.
           cameraRotation = Matrix.CreateFromAxisAngle(cameraRotation.Forward, RotationInRadians.Z)
               * Matrix.CreateFromAxisAngle(cameraRotation.Right, RotationInRadians.X)
               * Matrix.CreateFromAxisAngle(cameraRotation.Up, RotationInRadians.Y);
           // Make sure the camera is always pointing forward.
           Target = Position + cameraRotation.Forward;
           View = Matrix.CreateLookAt(Position, Target, cameraRotation.Up);
       }

我希望您已準備好上手實踐,因為您正在檢視為相機完成工作的程式碼。這在每一幀都會被呼叫,但所有這些都必須在每一幀上完成。Matrix 具有您在 3D 空間中可能需要的每個方法。為了在 3D 空間中旋轉某物,使用 Matrix 來確定您想要如何旋轉。當您按該順序將旋轉的 X、Y 和 Z 相乘時,它會使之成為現實。請注意 cameraRotation.Forward、Right 和 Up,這樣它就知道您希望如何計算。每個都對應於我們希望它如何顯示出來,相對於投射到螢幕上的相機的向上、向右和向前方向。在 3D 空間中旋轉物體可能會非常複雜,因此 XNA 使其儘可能簡單。當我們回到 Positioned Object 類時,您將看到我們是如何使該方法在每一幀自動呼叫。接下來,我們新增 Draw 方法,這將是您第一次看到此方法。請記住,正如一本偉大的書所說,“不要驚慌”。您將在相同區域內新增此方法,緊隨 Update 之後,如清單 5.5 中所示。

清單 5.5 Camera.cs 編輯

       public void Draw(BasicEffect effect)
       {
           effect.View = View;
           effect.Projection = Projection;
       }

現在您會注意到沒有覆蓋。這是因為繼承的類沒有 Draw 方法。如果您還記得,Positioned Object 類使用了 Draw Component。這意味著 Draw 方法將在每一幀自動呼叫。這樣就完成了我們的 Camera 類。一個類完成,還有兩個要完成。

這是完成的 Camera 類,如清單 5.6 所示。

清單 5.6 Camera.cs 目前

#region Using
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace Engine
{
   public class Camera : PositionedObject
   {
       #region Fields
       private Matrix cameraRotation;
       #endregion

       #region Properties
       public Matrix View
       {
           get;
           set;
       }

       public Matrix Projection
       {
           get;
           protected set;
       }

       public Vector3 Target
       {
           get;
           set;
       }
       #endregion

       #region Constructor
       public Camera(Game game, Vector3 position, Vector3 target, Vector3 rotation,
 bool Orthographic, float near, float far)
           : base(game)
       {
           Position = position;
           RotationInRadians = rotation;
           Target = target;

           if (Orthographic)
           {
               Projection = Matrix.CreateOrthographic(Game.Window.ClientBounds.Width,
Game.Window.ClientBounds.Height,
                   near, far);
           }
           else
           {
               Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
                   (float)Game.Window.ClientBounds.Width / (float)Game.Window.ClientBounds.Height,
near, far);
           }
       }
       #endregion

       #region Public Methods
       /// <summary>
       /// Allows the game component to perform any initialization it needs to before starting
       /// to run.  This is where it can query for any required services and load content.
       /// </summary>
       public override void Initialize()
       {
           base.Initialize();
           cameraRotation = Matrix.Identity;
       }

       /// <summary>
       /// Allows the game component to update itself via the GameComponent.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       public override void Update(GameTime gameTime)
       {
           base.Update(gameTime);
           // This rotates the free camera.
           cameraRotation = Matrix.CreateFromAxisAngle(cameraRotation.Forward, RotationInRadians.Z)
               * Matrix.CreateFromAxisAngle(cameraRotation.Right, RotationInRadians.X)
               * Matrix.CreateFromAxisAngle(cameraRotation.Up, RotationInRadians.Y);
           // Make sure the camera is always pointing forward.
           Target = Position + cameraRotation.Forward;
           View = Matrix.CreateLookAt(Position, Target, cameraRotation.Up);
       }

       public void Draw(BasicEffect effect)
       {
           effect.View = View;
           effect.Projection = Projection;
       }
       #endregion
   }
}

我認為是時候完成 Positioned Object 類了,我們只需要新增最後兩個方法,即 update 方法。因此,您現在需要將 PositionedObject.cs 檔案類調回到最前面。在 public 區域內,緊接 Initialize 類下方,新增 Update 類,如清單 6.1 中所示。

清單 6.1 PositionedObject.cs 編輯

       /// <summary>
       /// Allows the game component to be updated.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       public override void Update(GameTime gameTime)
       {
           base.Update(gameTime);
           frameTime = (float)gameTime.ElapsedGameTime.Seconds;
           Velocity += Acceleration * frameTime;
           Position += Velocity * frameTime;
           RotationVelocity += RotationAcceleration * frameTime;
           RotationInRadians += RotationVelocity * frameTime;
       }

我們這樣使用 frameTime float 的原因是,這裡只有一個外部呼叫,而不是四個。然後,我們使用總秒數內的經過遊戲時間來計算速度和加速度乘以秒數。這用於透過新增速度乘以秒數來計算當前位置。RotationVelocity 和 RotationAcceleration 也是如此。請注意,這完成了所有工作,並且它將對我們螢幕上繪製的每個遊戲物件執行此操作。我們不使用 Services 訪問相同的東西,因為那將是另一個外部類呼叫,它會進行另一個外部呼叫,並使用比一個更多的 CPU。當遊戲螢幕上的所有內容都使用此方法時,這些呼叫就會加起來。

這樣就完成了我們的 Positioned Object 類。這是完成的類,如清單 6.3 中所示。

清單 6.3 PositionedObject.cs 目前

#region Using
using System;
using Microsoft.Xna.Framework;
#endregion

namespace Engine
{
   public class PositionedObject : DrawableGameComponent
   {
       #region Fields
       private float frameTime;
       // Doing these as fields is almost twice as fast as if they were properties.
       // Also, sense XYZ are fields they do not get data binned as a property.
       public Vector3 Position;
       public Vector3 Acceleration;
       public Vector3 Velocity;
       public Vector3 RotationInRadians;
       public Vector3 ScalePercent;
       public Vector3 RotationVelocity;
       public Vector3 RotationAcceleration;
       #endregion

       #region Constructor
       /// <summary>
       /// This gets the Positioned Object ready for use, initializing all the fields.
       /// </summary>
       /// <param name="game">The game class</param>
       public PositionedObject(Game game)
           : base(game)
       {
           game.Components.Add(this);
       }
       #endregion

       #region Public Methods
       /// <summary>
       /// Allows the game component to be updated.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       public override void Update(GameTime gameTime)
       {
           base.Update(gameTime);
           frameTime = (float)gameTime.ElapsedGameTime.Seconds;
           Velocity += Acceleration * frameTime;
           Position += Velocity * frameTime;
           RotationVelocity += RotationAcceleration * frameTime;
           RotationInRadians += RotationVelocity * frameTime;
       }
       #endregion
   }
}

在進入第二章之前,您只需要完成最後一個類!您應該為自己能夠走到這一步而感到自豪。您離擁有一個遊戲引擎已經很近了!調出 Services 類,並在 public 方法區域內新增 Initialize 類,如清單 7.1 中所示。

清單 7.1 Services.cs 編輯

       /// <summary>
       /// This is used to start up Panther Engine Services.
       /// It makes sure that it has not already been started if it has been it will throw and exception
       /// to let the user know.
       ///
       /// You pass in the game class so you can get information needed.
       /// </summary>
       /// <param name="game">Reference to the game class.</param>
       /// <param name="graphicsDevice">Reference to the graphic device.</param>
       /// <param name="Camera">For passing the reference of the camera when instanced.</param>
       public static void Initialize(Game game, GraphicsDevice graphicsDevice, Camera camera)
       {
           //First make sure there is not already an instance started
           if (instance == null)
           {
               //Create the Engine Services
               instance = new Services(game);
               //Reference the camera to the property.
               Camera = camera;
               graphics = graphicsDevice;
               randomNumber = new Random();
               return;
           }

           throw new Exception("The Engine Services have already been started."); 
       }

這個方法,和其他的方法一樣,也是一個靜態方法。我們可以從遊戲中的任何類訪問它。我們會在遊戲類中新增對它的呼叫,傳入我們想要的數字。我將 Z 預設值設定為 20,這是一個很好的起始大小。Z 是指在我們的設定中,螢幕進出方向的平面。這在電子遊戲中是標準的做法。X 代表螢幕的上下方向,Y 當然代表左右方向。零點是螢幕的正中心。攝像機需要向後移動一段距離,以便我們可以看到我們放置在世界中的模型。現在,我們的 Services 類就完成了。在後面的章節中,我會讓你新增一些額外的輔助方法,比如計算空間中兩點之間角度的方法。我會在新增時解釋它們。以下是 listing 7.2 中完整的類。

Listing 7.2 Services.cs so far

#region Using
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
#endregion

namespace Engine
{
   public sealed class Services
   {
       #region Fields
       private static Services instance = null;
       private static GraphicsDevice graphics;
       private static Random randomNumber;
       #endregion

       #region Properties
       /// <summary>
       /// This is used to get the Services Instance
       /// Instead of using the mInstance this will do the check to see if the Instance is valid
       /// where ever you use it. It is also private so it will only get used inside the engine services.
       /// </summary>
       private static Services Instance
       {
           get
           {
               //Make sure the Instance is valid
               if (instance != null)
               {
                   return instance;
               }

               throw new InvalidOperationException("The Engine Services have not been started!");
           }
       }

       public static Camera Camera
       {
           get;
           private set;
       }

       public static GraphicsDevice Graphics
       {
           get { return graphics; }
       }

       public static Random RandomNumber
       {
           get { return randomNumber; }
       }
       /// <summary>
       /// Returns the window size in pixels, of the height.
       /// </summary>
       /// <returns>int</returns>
       public static int WindowHeight
       {
           get { return graphics.ScissorRectangle.Height; }
       }
       /// <summary>
       /// Returns the window size in pixels, of the width.
       /// </summary>
       /// <returns>int</returns>
       public static int WindowWidth
       {
           get { return graphics.ScissorRectangle.Width; }
       }
       #endregion
       #region Constructor
               /// <summary>
       /// This is the constructor for the Services
       /// You will note that it is private that means that only the Services can only create itself.
       /// </summary>
       private Services(Game game)
       {
           
       }
       #endregion

       #region Public Methods
       /// <summary>
       /// This is used to start up Panther Engine Services.
       /// It makes sure that it has not already been started if it has been it will throw and exception
       /// to let the user know.
       /// 
       /// You pass in the game class so you can get information needed.
       /// </summary>
       /// <param name="game">Reference to the game class.</param>
       /// <param name="graphicsDevice">Reference to the graphic device.</param>
       /// <param name="Camera">For passing the reference of the camera when instanced.</param>
       public static void Initialize(Game game, GraphicsDevice graphicsDevice, Camera camera)
       {
           //First make sure there is not already an instance started
           if (instance == null)
           {
               //Create the Engine Services
               instance = new Services(game);
               //Reference the camera to the property.
               Camera = camera;
               graphics = graphicsDevice;
               randomNumber = new Random();
               return;
           }

           throw new Exception("The Engine Services have already been started.");
       }
       #endregion
   }
}

再次開啟 Game1 類,我會讓你把它更新到最新狀態。首先找到 Game1 的建構函式,並在底部新增 listing 8.1.1 中的那一行。然後找到 Initialize 方法,並新增 listing 8.1.2 中的這一行:Listing 8.1.1 Game1.cs edit

           Camera = new Engine.Camera(this, new Vector3(0, 0, 275), Vector3.Forward,
Vector3.Zero, false, 200, 325);

Listing 8.1.2 Game1.cs edit

           Engine.Services.Initialize(this, graphics.GraphicsDevice, Camera);

這將啟動程式,用 200 的近平面和 325 的遠平面設定攝像機。這意味著所有距離攝像機 200 到 325 個單位之間,且在攝像機指向方向上的物體都會被繪製出來。我們將編輯 Update 方法;以下是 listing 8.2 中現在應該顯示的內容。我在所有遊戲中都這樣做,這樣我就可以直接按 Esc 鍵退出遊戲。事實上,我更改了預設的遊戲類,以便我不必新增這部分程式碼,我不知道為什麼它最初沒有這樣做。因為使用者總是會有鍵盤,但可能沒有 360 遊戲手柄。

Listing 8.2 Game1.cs edit

       /// <summary>
       /// Allows the game to run logic such as updating the world,
       /// checking for collisions, gathering input, and playing audio.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       protected override void Update(GameTime gameTime)
       {
           // Allows the game to exit
           if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed
               || Keyboard.GetState().IsKeyDown(Keys.Escape))
               this.Exit();

           base.Update(gameTime);
       }

我讓你更改了退出遊戲的首行程式碼。我添加了對 Esc 鍵的鍵盤輸入。滾動到頂部,在建構函式類中新增 listing 8.3 中的這一行

Listing 8.3 Game1.cs edit

           graphics = new GraphicsDeviceManager(this);

這意味著我們完成了第一章。以下是 listing 8.4 中 Game1 類現在應該顯示的內容。

Listing 8.4 Game1.cs so far

#region Using
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
#endregion

namespace Asteroids
{
   /// <summary>
   /// This is the main type for your game
   /// </summary>
   public class Game1 : Microsoft.Xna.Framework.Game
   {
       private GraphicsDeviceManager graphics;
       private Engine.Camera Camera;

       public Game1()
       {
           graphics = new GraphicsDeviceManager(this);
           Window.Title = "Asteroids 3D in XNA 4 Chapter One";
           graphics.PreferredBackBufferWidth = 1024;
           graphics.PreferredBackBufferHeight = 600;
           graphics.ApplyChanges();
           
           Content.RootDirectory = "Content";
           // Here we instance the camera, setting its position, target,
rotation, whether it is orthographic,
           // then finally the near and far plane distances from the camera.
           Camera = new Engine.Camera(this, new Vector3(0, 0, 275), Vector3.Forward,
Vector3.Zero, false, 200, 325);
       }

       /// <summary>
       /// Allows the game to perform any initialization it needs to before starting to run.
       /// This is where it can query for any required services and load any non-graphic
       /// related content.  Calling base.Initialize will enumerate through any components
       /// and initialize them as well.
       /// </summary>
       protected override void Initialize()
       {
           Engine.Services.Initialize(this, graphics.GraphicsDevice, Camera);

           base.Initialize();
       }

       /// <summary>
       /// LoadContent will be called once per game and is the place to load
       /// all of your content.
       /// </summary>
       protected override void LoadContent()
       {

       }

       /// <summary>
       /// UnloadContent will be called once per game and is the place to unload
       /// all content.
       /// </summary>
       protected override void UnloadContent()
       {
           // TODO: Unload any non ContentManager content here
       }

       /// <summary>
       /// Allows the game to run logic such as updating the world,
       /// checking for collisions, gathering input, and playing audio.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       protected override void Update(GameTime gameTime)
       {
           // Allows the game to exit
           if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed
               || Keyboard.GetState().IsKeyDown(Keys.Escape))
               this.Exit();

           base.Update(gameTime);
       }

       /// <summary>
       /// This is called when the game should draw itself.
       /// </summary>
       /// <param name="gameTime">Provides a snapshot of timing values.</param>
       protected override void Draw(GameTime gameTime)
       {
           GraphicsDevice.Clear(new Color(5, 0, 10));

           base.Draw(gameTime);
       }
   }
}
圖十四

你應該能夠點選執行,並看到圖 14 中所示的空白午夜紫色螢幕,這意味著它正在工作。我們還沒有任何東西顯示,但如果它沒有出現錯誤,那麼你很可能做得正確。除了 using 之外,我沒有在 Game1 類中新增區域,因為它佔用了太多空間,而且你不需要看到它。如果你正確地編寫了你的遊戲,你就不會在遊戲類中做太多事情。到目前為止,我建立這個教程玩得很開心,希望你也很享受跟我一起建立它的過程。你應該有一個包含整個專案的壓縮檔案。 Chapter One Project File 如果你遇到任何問題,請將包含的專案壓縮檔案解壓縮到你的 Visual Studio 資料夾中,或者解壓縮到文件中的任何位置。你將找到到目前為止完成的專案,你可以開啟並檢查。

謝謝,祝遊戲愉快!要繼續 第二章

華夏公益教科書