共用方式為


XNA 實驗室

使用 XNA 框架進行 3D 遊戲開發

 

概觀

本次實驗將向你介紹在 Windows Phone 7™ 上的 3D 遊戲開發和使用 XNA Game Studio  進行遊戲開發的基礎。

在本次實驗中,你將使用 XNA Game Studio 構建一個簡單的,尚不完整的 3D 遊戲來讓你熟悉 XNA Game Studio 3D 遊戲開發的基本概念。你還可以學習到如何使用帶 Windows Phone 7™SDK 的 Microsoft Visual 2010 Express 為 Windows Phone 7™ 平台構建和設計你的 XNA 遊戲。


實驗目標

本次上機實驗,你將會學得:

  • 了解 Windows Phone 7™ 裡 XNA 遊戲引擎模型。
  • 如何在你的 XNA Game Studio 遊戲中導入,處理和使用 3D 資源 (3D 模型,紋理圖案,圖片,字體,音效檔等)。
  • 理解 Windows Phone 7™ 遊戲的基本繪畫機制,包括燈光效果和攝影機運動。
  • 如何使用設備的觸控 (Touching) 和加速 (accelerometer) 功能來控制遊戲,及使用 Windows Phone 7™ 模擬器時如何處理電腦輸入來模擬控制。
  • 如何添加基本的遊戲邏輯和基本的 3D 物理學 (移動,旋轉,碰撞檢測等)。
  • 如何在你的遊戲裡添加聲音效果。
  • 如何為你的遊戲建立並管理多個遊戲畫面。

前置條件

以下是完成本次上機實驗所必須的條件:

  • 用於 Windows Phone 的 Microsoft Visual Studio 2010 Express 或者內含 Windows Phone 7™ 外掛程式的 Microsoft Visual Studio 2010。

任務

本次上機實驗包括來自下列任務的兩個練習:

練習 1

  1. 建立一個支援遊戲狀態管理 (Game State Management) 的遊戲專案。
  2. 載入,佈置和繪製 3D 模型。
  3. 處理使用者輸入,旋轉 3D 物件,3D 空間的基本運動和追逐相機。
  4. 碰撞檢測和遊戲物理學 (加速度,摩擦,速度,角速度等) 。

練習 2

  1. 添加畫面和功能表。
  2. 添加聲音效果。
  3. 管理高分表包括內容提交到儲存區裡。
  4. 為加速器添加一個校準畫面。

本次實驗完成需要:120分鐘。


練習 1:支援遊戲狀態管理 (Game State Management) 的 XNA Game Studio 遊戲基礎

如果你曾經想製作你自己的遊戲,Microsoft® XNA® Game Studio 4.0 就是為你量身定做的。

不管你是學生,遊戲愛好者還是獨立的遊戲開發人員,你都可以透過 XNA Game Studio 製作並分享偉大的遊戲。

XNA Game Studio 4.0 是一款遊戲開發產品,它基於 Windows Phone 7™的Microsoft Visual Studio 2010 Express,允許遊戲開發者使用基於 .NET 的程式設計語言,比如 C# 和強大並健壯的 Visual Studio 2010 作為 IDE 來開發他們的遊戲。

  • XNA Game Studio 4.0 包括 XNA 框架和 XNA Framework 內容管道。

**The XNA Framework:**是個執行期引擎 (Runtime Engine) 和類別庫 (.NET runtime 和類別庫的擴充) 來提供健壯的遊戲應用程式設計介面,它簡化了為 Xbox 360™、基於 Windows®  平台的電腦和現在的 Windows Phone 7™Series® 的遊戲開發任務。

**The XNA Content Pipeline:**是整合到遊戲開發環境中的一個內容導入器和處理器的集合。它提供了為把 3D 模型,紋理圖案 (textures),圖片,聲音和其他資源導入到你的遊戲提供了一種簡單而靈活的方式。XNA 內容管道是可擴充的,允許你建立自己的內容導入器和處理器來支援任何類型和格式的資源或者在載入過程中向現有的資源類型添加自訂資料。

 XNA Game Studio 確實是一個易用的開發環境和程式設計框架。Microsoft 開發XNA Game Studio 是輔助開發人員更快更容易的製作遊戲。但是,它不是一個可以拖曳的視覺化遊戲開發工具,而是一個程式設計開發環境,所以你需要會 C# 程式設計並具備物件導向程式設計的技能,才能使用它。

XNA Framework 不是一個遊戲引擎。它不包括攝影機管理,狀態/ 畫面/ 等級管理,物理學,碰撞監控或其他可以在遊戲引擎中找到的東西。它是一個遊戲開發框架,也就意味著如何有序的工作完全取決於你的程式設計。

在本次實驗中,你將建立一個完整的 Windows Phone 7™ 上的 3D 遊戲。你建立的遊戲“迷宮彈球 (Marble Maze)”是一款單人遊戲,在遊戲中玩家以盡可能最少的時間來引導彈球穿過 3D 的迷宮並到達終點,同時要避免彈球掉進地面的洞中 (這會使彈球在最後的訪問點重生)。玩家利用傾斜設備影響遊戲迷宮的傾斜來使彈球滾動透過迷宮。一旦玩家到達迷宮的終端,結果時間就會跟設備上儲存的最快時間作比較。如果時間是前十名的話,會允許玩家把名字記錄到高分表中。

XNA Game Studio 遊戲基礎

通常遊戲有 3 個階段:

  • Initializing and Loading – 在這個階段,我們載入資源,初始化遊戲相關變數並執行其他必須在遊戲起始之前必須執行的任務。這個階段在遊戲的生命週期中只發生一次。
  • Update – 在這個階段,我們更新遊戲世界的狀態。通常意味著根據遊戲物理學來計算遊戲物件的新位置 / 方向,處理用戶輸入並做相對回應,觸發聲音效果,更新健康狀態,彈藥和其他狀態,更新分數並執行其他遊戲相關的邏輯。作為遊戲主邏輯的一部分,只要遊戲引擎持續執行,這個狀態就一直會重複的發生。
  • Draw – 在這個階段,我們把輸出圖形的設備作為視覺框 (Frame),在上面繪製遊戲場景來顯示遊戲當前的狀態。作為遊戲主邏輯的一部分,只要遊戲引擎持續執行,這個狀態就一直會重複的發生。

在 XNA Framework 中,Update 和 Draw 狀態會在 PC 或 Xbox 360 上以每秒 60 次的速度發生,在 Zune,Zune HD 或 Windows Phone 7 設備上以每秒 30 次的速度發生

總體架構

 "迷宮彈球" 遊戲使用的遊戲畫面管理架構來自於 Game State Management 的範例 (最初來源於 https://creators.xna.com/en-US/sample/phonegamestatemanagement),為本次實驗提供了資源。這個遊戲包括以下畫面:

  • 主功能表畫面 (MainMenuScreen 類別) 。
  • 高分表畫面 (HighScoreScreen 類別)。
  • 遊戲畫面 (GameplayScreen 類別)。
  • 暫停 (PauseScreen 類別)。
  • 加速器校準畫面 (CalibrationScreen 類別)。

在顯示遊戲畫面之前,遊戲會載入遊戲內容,這樣在遊戲開始之前就避免了明顯的延遲。

一旦執行起來,遊戲的第一個動作是載入並顯示背景畫面,然後是主功能表。一旦遊戲主功能表載入完畢,使用者就可以訪問遊戲或查看高分表。

完整的遊戲畫面如下所示:


圖例 1
完成版本的迷宮彈球遊戲

任務 1 – 建立支援遊戲狀態管理的基本遊戲專案

在本任務中,你將為 Windows Phone 7™ 平台建立一個 XNA Game Studio 遊戲專案並透過加入本實驗提供的程式碼是遊戲專案具備 game state management 的能力。

1. 啟動用於 Windows Phone 的 Visual Studio 2010 Express 或 Visual Studio 2010。

**說明:**本次上機實驗的步驟闡明了使用內建 Windows Phone 開發工具的 Microsoft Visual Studio 2010 的使用過程,但是它們對 Microsoft Visual Phone Developer 2010 Express 同樣適用。通常情況下,所有的操作步驟都適用於這兩款產品。

2. 從 Start | All Programs | Microsoft Visual Studio 2010 Express 打開 Microsoft Visual Phone Developer 2010 Express。

Visual Studio 2010:Start | All Programs | Microsoft Visual Studio 2010 打開 Visual Studio 2010。

3. 在 File 功能表中,選擇 New Project

**Visual Studio 2010:**在 File 功能表中,點擊 New,然後選擇 Project

4. 在 New Project 對話方塊中,選擇 XNA Game Studio game for Windows Phone 類別,並且在已安裝的範本清單中選擇 Windows Phone Game (4.0)。然後輸入名字 MarbleMazeGame,點擊 OK


圖例 2
在 Microsoft Visual Studio 2010 裡建立一個新的 Windows Phone 遊戲專案

5. 在 Solution Explorer 裡,檢查由 Windows Phone 應用範本產生的解決方案的結構。任何 Visual Studio 解決方案都是一個包括相關專案的容器。在本案例中,它包括一個名為 MarbleMazeGame 的 XNA Game Studio game for Windows Phone 的專案和一個名為 MarbleMazeGameContent 的相關遊戲資源專案。


圖例 3
Solution Explorer 顯示 MarbleMazeGame 解決方案

**說明:**Solution Explorer 允許你在一個解決方案或專案裡查看項目並執行項目管理任務。按 CTRL + W,S 或在 View 功能表中,選擇 Other Windows | Solution Explorer,可以打開 Solution Explorer。

6. 生成的專案包括一個預設的遊戲實作,這個遊戲實現包括基本的 XNA Game Studio 遊戲迴圈 (Game Loop)。它位於 Game1.cs 文件裡。

7. 打開 Game1.cs 文件。我們建議你將預設的名字改成一個可以反映你遊戲的名字 。

8. 重新命名主遊戲類別名 (預設的名字Game1) 為 MarbleMazeGame。若要重新命名,在類別名稱上點右鍵,選擇 Refactor | Rename


圖例 4
重新命名主遊戲類別名稱

9. 在重新命名對話方塊 New name 的欄位中,輸入 MarbleMazeGame 並點擊 OK


圖例 5
為主遊戲類別命名

10. 檢查 Visual Studio 推薦的修改地方並點擊 Apply。


圖例 6
對主遊戲類別應用修改

11. 重新命名檔案名稱與新的類別名稱對應。在 Solution Explorer 裡的 Game1.cs 按右鍵並選擇 Rename。把檔案重新命名為 MarbleMazeGame.cs。


圖例 7
重新命名主遊戲類別檔案

  • 一個 XNA Game Studio game for Windows Phone 的應用典型地利用了基本平台或其他類別庫提供的服務。要使用這些功能,應用需要引用實現這些服務的相應組件 (assembly)。

12. 為了顯示專案引用的組件,在 Solution Explorer 裡展開 References 節點並檢查列表。列表包括常規的 XNA Framework 組件和專門用於 Windows Phone 平台的組件。


圖例 8
Solution Explorer 顯示專案引用的組件

目前,這個應用程式沒做太多的事,但是為第一次測試執行做好了準備。在這步驟中,你建立應用程式並部署到 Windows Phone 模擬器上,然後執行它來理解標準的開發流程。

13. 在 View 功能表中,選擇 Output 來打開Output 視窗。

14. 在 Debug 功能表中選擇 Build Solution 或者按下 SHIFT + F6 複合鍵來編譯解決方案裡的專案。

**Visual Studio 2010:**在 Build 功能表中選擇 Build Solution 或者按下 CTRL + SHIFT + B 來編譯解決方案裡的專案。

15. 觀察 Output 視窗並檢查在過程中產出的追蹤資訊包括它輸出結果的最終資訊。


圖例 9
在 Visual Studio 裡建置應用

在這個階段,你應該看不到任何錯誤,但是如果專案包含編譯錯誤,這些錯誤將會顯示在 Output 視窗。你可以利用 Error List  視窗來處理這些錯誤。這個視窗會以清單的形式顯示編譯器產生的錯誤,警告和資訊,你可以根據錯誤的嚴重層級進行排序過濾。此外,你可以按兩下列表裡的項目來自動地打開相關的原始程式碼檔並追蹤錯誤的原因。

16. 要想打開錯誤清單視窗,你只要在 View 功能表中,找到 Other Windows 並選擇 Error List。

**Visual Studio 2010:**在 View 功能表中選擇 **Error List,**來打開錯誤清單視窗。


圖例 10
錯誤清單視窗顯示編譯過程中產生的錯誤

**說明:**要明白在現在這個階段,你不應該遇到任何錯誤。上面的步驟僅僅是解釋如何訪問錯誤清單視窗。

17. 驗證部署的目標設備是 Windows Phone 模擬器。要做到這一點,確保在工具列 Start Debugging 按鈕旁邊的 Select Device 下拉清單中選擇 Windows Phone 7 模擬器。


圖例 11
選擇目標設備來部署應用

**說明:**當你從 Visual Studio 中部署你的應用程式時,你可以選擇部署到真實的設備上還是 Windows Phone 模擬器。

18. 在 Windows Phone 模擬器中按 F5 執行應用程式 ,會出現一個設備模擬器視窗,當 Visual Studio 搭建模擬器環境並部署程式時,它會處於暫停狀態。一旦準備完畢,模擬器會展示開始介面,片刻之後,你的應用程式便會顯示在模擬器視窗中。

應用程式將會顯示一個不包括任何東西的簡單藍色介面。在這個初期階段是正常的。


圖例 12
在 Windows Phone 7™ 模擬器中執行應用程式

你基本上無法對應用程式進行操作,直到你建立了使用者介面並編寫了應用程式的邏輯。

19. 按下 SHIFT + F5 或者在工具列上點擊 Stop 按鈕來卸除除錯器並結束除錯程序。不要關閉模擬器窗口。


圖例 13
結束除錯程序

**提示:**開啟一個除錯程序,會在模擬環境建置和應用程式執行上花費一些時間。為了簡化除錯體驗,請不要在 Visual Studio 裡處理原始程式碼時關閉模擬器。一旦模擬器啟動 ,它將會用很少的時間停止當前程序,編輯原始程式碼,然後編譯並部署一個新版的應用程式來啟動一個新的除錯程序。

現在我們遊戲雛形可以執行了。現在向裡面添加遊戲狀態管理的能力。這將有助於在接下來的任務中向遊戲添加螢幕和功能表。

20. 添加一個新的專案資料夾來包含所有的遊戲狀態管理程式碼。在solution explorer 裡,右鍵點擊 MarbleMazeGame 節點並從快顯功能表 (context menu) 中選擇 Add | New folder


圖例 14
添加一個新的專案資料夾

21. 建立一個名為 ScreenManager 的資料夾。

22. 選擇 ScreenManager 資料夾並將 Source\Assets\Code\ScreenManager 下面本實驗安裝資料夾裡的所有檔添加進去。要添加已有的項目,在 SolutionExplorer 以右鍵點按 ScreenManager,然後選擇 Add 並點按 Existing items,將出現一個檔案選擇對話方塊:


圖例 15
向專案裡添加已有的項目

23. 此時會彈出檔案選擇視窗。沿著以前步驟規定的路徑,選擇所有檔,並按 Add 按鈕:


圖例 16
向專案裡添加 ScreenManager 原始檔案

**說明:**實驗安裝資料夾下包括所有遊戲資源和範例程式碼,具體位置如下:

{LAB_PATH}\Assets\Code – 所有CSharp 程式碼檔。

{LAB_PATH}\Assets\Media – 所有圖像,字體和聲音。

**說明:**這個步驟添加的程式碼實作了為建立 XNA Game Studio 功能表和螢幕的 Windows Phone Game State Management 樣本。我們建議你溫習一下這個範例來加深理解。你可以在:https://creators.xna.com/en-US/sample/phonegamestatemanagement 找到完整的範例。

注意,我們已經對本實驗的程式碼進行了少量的修改。

24. 回顧 SolutionExplorer,你的 Solution Explorer 畫面應該如下所示:


圖例 17
在添加 ScreenManager 資料夾和程式碼之後的 solution explorer

25. ScreenManager 的程式碼依賴於定義了背景圖片和功能表字體的現有資源。我們將利用這個機會向遊戲中添加所有字體和文字資源 。使用檔案總管瀏覽到實驗安裝資料夾下的 Assets \ Media。

26. 在檔案總管中,選擇 “Fonts” 和 “Textures” 資料夾並拖曳到 SolutionExplorer 裡的 MarbleMazeGameContent 專案節點裡。


圖例 18
向內容專案中添加資源檔夾

**說明:**拖曳操作是在兩個應用程式之間進行的。先是從檔案總管拖動,然後再放到 Visual Studio 的 MarbleMazeGameContent 裡面。

27. 再次編譯解決方案,應該沒有任何編譯錯誤。因為我們還沒做任何明顯修改,所以沒必要再次執行應用程式。


任務 2 – 3D 繪圖 (Drawing)

在本任務中,你將增強你的 MarbleMazeGame 遊戲專案並向其添加 3D 繪圖功能。

在我們繪製 3D 模型之前,需要完全理解 Windows Phone 7 的 3D 坐標軸系統。

傳統的 3 大坐標軸代表了 Windows Phone 7 的坐標系統:X,Y,和 Z。

沿著 X 軸移動代表從左到右的過程,因此當我們向右走時 X 值增加,反之亦然。

Y 軸的作用類似於 X 軸,從下到上,當我們向上移動時,Y 值增加。

Z 軸代表縱深維度。當我們從虛擬圖點向手機螢幕移動時,Z 值增加,反之亦然。


圖例19
直向模式 (Portrait Mode) 的 X,Y,Z 軸

上面的圖例 說明了直向模式中的坐標軸系統。和下個圖例顯示在風景模式中繪製的坐標軸系統一樣,手機當前的位置不會改變坐標軸系統,因為 Y 軸一直代表從低到高的繪製,X 軸代表從左到右,Z 軸代表從手機到用戶。


圖例 20
橫向模式 (Landscape Mode) 中的 X, Y,Z 軸

上面主要的含義是不管手機的位置如何,螢幕繪製是一直在執行的 (圖片一直被正確的校準),手機用戶看起來很直覺。

NOTE

在中文簡體翻譯中,直向模式翻譯為「肖像模式」,橫向模式翻譯為「風景模式」。

但是程式師在螢幕上繪製 (Rendering) 3D 模型,不可能完全忽視設備的方法,因為當前的方向會改變繪圖的比例。

1. 使用 Visual Studio 2010 打開專案並查看解決方案 - 它現在包括一個 "MarbleMazeGame" 的專案,裡面包含了遊戲應用和邏輯還包含了遊戲內容的完整集合。

  • 現在我們想添加遊戲物件元素,它可以在 3D 環境中顯示 3D 模型和功能。
  • 為了這個目的,我們應該建立一個從 "DrawableComponent" 類別衍生出的新類別,它是一個需要時可以繪圖的遊戲元件。

但是有幾方面的 3D 繪製是 "DrawableComponent" 類別做不了的,比如載入並繪製 3D 模型。因此,我們必須建立一個衍生類別,並命名為 "DrawableComponent3D"。但是在我們操作之前,我們需要定義一個 “Camera” 物件。Camera 用於定義當前視點 (view point),它是個不可見的遊戲組件。

2. 添加一個新的專案資料夾包括所有 3D 物件程式碼。在 solution explorer,右鍵點擊 “MarbleMazeGame” 節點並從快顯功能表中選擇 Add | New folder。

3. 建立一個名為 Objects 的資料夾。

4. 向 Objects 資料夾中添加一個名為 “Camera” 的新類別。為了做到這一點,右鍵點擊剛剛建立的 “Objects” 資料夾並選擇 Add | Class


圖例 21
向專案資料夾中添加一個新類別

5. 在顯示的對話方塊中,把類別命名為 “Camera” 並點擊 Add


圖例 22
給新類別命名

  • 打開類別檔,這個檔應該位於 “Objects” 資料夾下。你會看到這個檔只包含了一些基本的 "using" 陳述句和類別定義。在文件開頭添加如下 "using" 陳述句。

6. 你可以從下面程式碼裡直接複製粘貼:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 6 – CameraUsing statements)

C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

7. 把新類別改成 GameComponent 的衍生類別 (在 "Microsoft.Xna.Framework" 命名空間中定義的)。我們使用父類別來繼承一些方法集合,它將從邏輯上安裝 Camara 作為遊戲元件,但是並不在螢幕上顯示。修改 "MarbleMazeGame" 中類別的命名空間並設為 public 類別:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 7 – CameraClass Definition)。

C#

namespace MarbleMazeGame
{
    public class Camera : GameComponent
    {
    }
}

**說明:**本實驗只使用一個命名空間 – MarbleMazeGame。預設情況下當在專案中建立一個新的專案項時,Visual Studio 會把相關的資料夾作為命名空間。去掉這種自動建立的命名空間並用預設的命名空間 – MarbleMazeGame 取代。

8. 向類別中添加如下程式碼,用於定義視點位置和投影 (projection)。遺憾的是,視點和投影的解釋在本次實驗的範圍之外。這些在 3D 渲染背景材料中是相當標準的術語:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 8 – Camera Fields)

C#

#region Fields
Vector3 position = new Vector3(0, 1000, 2000);
Vector3 target = Vector3.Zero;
GraphicsDevice graphicsDevice;

public Matrix Projection { get; set; }
public Matrix View { get; set; }
#endregion

#region Initializtion
public Camera(Game game, GraphicsDevice graphics)
    : base(game)
{
    this.graphicsDevice = graphics;
}

/// <summary>
/// Initialize the camera
/// </summary>
public override void Initialize()
{
    // Create the projection matrix
    Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(50),
        graphicsDevice.Viewport.AspectRatio, 1, 10000);

    // Create the view matrix
    View = Matrix.CreateLookAt(position, target, Vector3.Up);
    base.Initialize();
}
#endregion

NOTE

如果對 3D 程式設計有興趣,可以參考 Windows 大師 Charles Petzold 的著作 ”3D Programming for Windows”,或是國內作者所編著的 3D 遊戲設計相關書籍。

若是只對 3D 的術語有興趣,可以參考電腦圖學相關的教科書,或是到維基百科查詢:http://zh.wikipedia.org/wiki/三維計算機圖形

9. 現在我們擁有了可以查看 3D 物件的 camera ,可以建立物件本身了。在 “Objects” 專案資料夾下建立一個新類別並命名為 “DrawableComponent3D” (這個類別將會實現步驟 1 中提到的功能)。

10. 打開新的類別檔案並在檔案開頭添加如下宣告語句:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 10 – DrawableComponent3DUsing statements)

C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections;
using System.Collections.Generic;

11. 修改 DrawableComponent3D 類別,使其成為 DrawableGameComponent 類別的衍生類別,並像前面所做的一樣,把命名空間改成 "MarbleMazeGame"。並且把類別改成 public 和 abstract:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 11 – DrawableComponent3DClass Definition)

C#

namespace MarbleMazeGame
{
    public abstract class DrawableComponent3D : DrawableGameComponent
    {
    }
}

**說明:**記得將新類別的命名空間改為”MarbleMazeGame”。

12. 添加如下類別變數,用於以後的 3D 圖片繪製:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 12 – DrawableComponent3D Fields)

C#

string modelName;
protected bool preferPerPixelLighting = false;
public Model Model = null;
public Camera Camera;

public Vector3 Position = Vector3.Zero;
public Vector3 Rotation = Vector3.Zero;

public Matrix[] AbsoluteBoneTransforms;
public Matrix FinalWorldTransforms;
public Matrix OriginalWorldTransforms = Matrix.Identity;

13. 定義建構函式如下:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 13 – DrawableComponent3D Constructor)

C#

public DrawableComponent3D(Game game, string modelName)
    : base(game)
{
    this.modelName = modelName;
}

上面的程式碼只是給 "modelName" 賦值以備後續之用。

14. 覆寫父類別的 LoadContent 方法來載入真實的 3D 模型資源:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 14 – DrawableComponent3D LoadContent)

C#

protected override void LoadContent()
{
    // Load the model
    Model = Game.Content.Load<Model>(@"Models\" + modelName);

    // Copy the absolute transforms
    AbsoluteBoneTransforms = new Matrix[Model.Bones.Count];
    Model.CopyAbsoluteBoneTransformsTo(AbsoluteBoneTransforms);

    base.LoadContent();
}

上面程式碼從遊戲內容專案中載入物件模型 (我們將在後面的階段向內容專案添加這些模型) 並且為了合適的定位來轉換模型。

15. 透過覆寫 Draw 方法在類別中添加自訂的 3D 繪製邏輯:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 15 – DrawableComponent3D Draw)

C#

public override void Draw(GameTime gameTime)
{
    foreach (ModelMesh mesh in Model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            // Set the effect for drawing the component
            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = preferPerPixelLighting;

            // Apply camera settings
            effect.Projection = Camera.Projection;
            effect.View = Camera.View;
                    
            // Apply necessary transformations
            effect.World = FinalWorldTransforms;
        }

        // Draw the mesh by the effect that set
        mesh.Draw();
    }

    base.Draw(gameTime);
}

上面的程式碼根據當前的狀態更新元件轉換模型。我們將在下個練習中更新 3D 物件的狀態。

17. 現在我們有了一個 "DrawableComponent3D" 類別,接下來,我們將建立 "Maze" 和 "Marble" 衍生類別來管理和顯示相應的 3D 物件。在 "Objects" 資料夾中添加一個新類別,並命名為 "Marble"。

18. 在新的類別檔開頭添加如下宣告:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 18 – Marble Using statements)

C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;

19. 修改新類別,使其成為 DrawableComponent3D 的衍生類別。記住修改類別的命名空間。

20. 在類別中添加如下欄位和建構函式。這些欄位將用於保存彈球的圖案:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 20 – Marble Fields and Constructor)

C#

private Texture2D m_marbleTexture;

public Marble(Game game)
    : base(game, "marble")
{
    preferPerPixelLighting = true;
}

21. 因為"DrawableComponent3D" 已經支援 3D 物件的更新和繪製了,我們只需要在衍生物件中擴充功能。向覆寫的 LoadContent 方法裡添加如下程式碼,除了基本的功能外,還用於載入彈球的圖案:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 21 – Marble LoadContent)

C#

protected override void LoadContent()
{
    base.LoadContent();

    // Load the texture of the marble
    m_marbleTexture = Game.Content.Load<Texture2D>(@"textures\Marble");
}

22. 添加一個覆寫的 Draw 方法來用繪製彈球的程式碼替換父類別中的實作:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 22 – MarbleDraw)

C#

public override void Draw(GameTime gameTime)
{
    var originalSamplerState = GraphicsDevice.SamplerStates[0];

    // Cause the marble's textures to linearly clamp            
    GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;

    foreach (var mesh in Model.Meshes)
    {
        foreach (BasicEffect effect in mesh.Effects)
        {
            // Set the effect for drawing the marble
            effect.EnableDefaultLighting();
            effect.PreferPerPixelLighting = preferPerPixelLighting;
            effect.TextureEnabled = true;
            effect.Texture = m_marbleTexture;

            // Apply camera settings
            effect.Projection = Camera.Projection;
            effect.View = Camera.View;

            // Apply necessary transformations
            effect.World = AbsoluteBoneTransforms[mesh.ParentBone.Index] *
                FinalWorldTransforms;
        }

        mesh.Draw();
    }

    // Return to the original state
    GraphicsDevice.SamplerStates[0] = originalSamplerState;
}

**說明:**我們不需要覆寫 "Update" 方法,因為 "DrawableComponent3D" 類別裡的實作已滿足我們的需要。

23. 接下來,在 "Objects" 專案資料夾下添加一個 "Maze" 類別。

24. 在新的類別檔開頭添加如下宣告:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 24 – Maze Using statements)

C#

using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

25. 在 “Maze.cs” 檔裡用下面的程式碼替換“Maze” 的類別定義:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 25 – Maze Class)

C#

class Maze : DrawableComponent3D
{
    public Maze(Game game)
        : base(game, "maze1")
    {
        preferPerPixelLighting = false;
    }

    public override void Draw(GameTime gameTime)
    {
        var originalSamplerState = GraphicsDevice.SamplerStates[0];

        // Cause the maze's textures to linearly wrap            
        GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;

        foreach (var mesh in Model.Meshes)
        {
            foreach (BasicEffect effect in mesh.Effects)
            {
                // Set the effect for drawing the maze
                effect.EnableDefaultLighting();
                effect.PreferPerPixelLighting = preferPerPixelLighting;

                // Apply camera settings
                effect.Projection = Camera.Projection;
                effect.View = Camera.View;

                // Apply necessary transformations
                effect.World = AbsoluteBoneTransforms[mesh.ParentBone.Index] *
                    FinalWorldTransforms;
            }

            mesh.Draw();
        }

        // Return to the original state
        GraphicsDevice.SamplerStates[0] = originalSamplerState;
    }
}

上面的程式碼非常類似於我們在 “Marble” 類別中所做的工作。

我們基本設定完了。迷宮和彈球的 3D 模型是必要的,因為我們即將用到這些物件。

26. 在 “MarbleMazeGameContent” 專案裡建立一個新的專案資料夾,然後把 Assets \ Media \ Models 下的實驗安裝資料夾中的所有檔案都添加到新資料夾中。

27. 最後,我們需要建立包括並繪製所有物件的 gameplay screen 。在 “MarbleMazeGame” 專案下建立一個新的專案資料夾並命名為 “Screens”。

28. 在已建立的 “Screens” 專案檔中添加新類別,並命名為 “GameplayScreen”。

29. 打開新的類別檔並在開頭添加如下宣告:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 29 – GameplayScreen Using statements)

C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using GameStateManagement;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Audio;

30. 修改 GameplayScreen 類別,使其繼承 GameScreen 類別。GameScreen 類別是在以前添加的 ScreenManager 程式碼裡定義的。

**說明:**你記住修改類別的命名空間了嗎?

31. 向 GameplayScreen 類別中添加如下欄位,用於保存我們遊戲物件的執行個體:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 31 – GameplayScreen Fields)

C#

Maze maze;
Marble marble;
Camera camera;

32. 在類別中添加如下建構函式:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 32 – GameplayScreen Constructor)

C#

public GameplayScreen()
{
    TransitionOnTime = TimeSpan.FromSeconds(0.0);
    TransitionOffTime = TimeSpan.FromSeconds(0.0);
}

上面的程式碼透過設定從父類別中繼承過來的屬性來控制畫面進出轉換的方式。

33. 在 GameplayScreen 類別中添加如下方法的集合:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 33 – GameplayScreen Methods)

C#

public override void LoadContent()
{
    LoadAssets();
 
    base.LoadContent();
}
 
public void LoadAssets()
{
    InitializeCamera();
    InitializeMaze();
    InitializeMarble();
}
 
private void InitializeCamera()
{
    // Create the camera
    camera = new Camera(ScreenManager.Game, ScreenManager.GraphicsDevice);
    camera.Initialize();
}
 
private void InitializeMaze()
{
    maze = new Maze(ScreenManager.Game)
    {
        Position = Vector3.Zero,
        Camera = camera
    };
 
    maze.Initialize();
}
 
private void InitializeMarble()
{
    marble = new Marble(ScreenManager.Game)
    {
        Position = Vector3.Zero,
        Camera = camera
    };
 
    marble.Initialize();
}

這些新方法只是在畫面載入階段初始化各種 3D 物件。

34. 透過加入下面的覆寫 Update 方法,向 gameplay screen 類別中添加自訂的更新和繪製邏輯:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 34 – GameplayScreenUpdate and Draw)

C#

public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
    // Update all the component of the game
    maze.Update(gameTime);
    marble.Update(gameTime);
    camera.Update(gameTime);
}

public override void Draw(GameTime gameTime)
{
    ScreenManager.GraphicsDevice.Clear(Color.Black);
    ScreenManager.SpriteBatch.Begin();
 
    // Drawing sprites changes some render states around, which don't play
    // nicely with 3d models. 
    // In particular, we need to enable the depth buffer.
    DepthStencilState depthStensilState =
 new DepthStencilState() { DepthBufferEnable = true };
    ScreenManager.GraphicsDevice.DepthStencilState = depthStensilState;
 
    // Draw all the game components
    maze.Draw(gameTime);
    marble.Draw(gameTime);
 
    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

這些覆寫方法把大多數工作都放到了 3D 物件本身以在螢幕上繪製它們。

35. 最後,我們必須修改主遊戲類別來讓 gameplay screen 使用 ScreenManager 類別。打開 “MarbleMazeGame.cs” 檔並用下面的程式碼替換裡面的整個內容:

(Code Snippet – 3D Game Development – Ex 1 Task 2 Step 35 – MarbleMazeGame)

C#

using System;
using Microsoft.Xna.Framework;
using GameStateManagement;
 
namespace MarbleMazeGame
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class MarbleMazeGame : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        ScreenManager screenManager;

        public MarbleMazeGame()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
 
            // Frame rate is 30 fps by default for Windows Phone.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
            //Create a new instance of the Screen Manager
            screenManager = new ScreenManager(this);
            Components.Add(screenManager);
 
            // Switch to full screen for best game experience
            graphics.IsFullScreen = true;
 
            graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft;
 
            screenManager.AddScreen(new GameplayScreen(),null);
        }
    }
}

這個版本的遊戲類別只是使用 ScreenManager 類別在遊戲裡添加一個 GameplayScreen 類別。

36. 編譯並部署專案。你看到的應該是遊戲的 3D 物件,迷宮和彈球而不是一個空畫面。


圖例 23
在螢幕上渲染的遊戲物件


任務 3 – 3D 移動和 Camara

雖然我們現在的遊戲在螢幕上向使用者呈現了各種遊戲物件,但是它還不是一個用戶可以交互的遊戲。最終的遊戲是用戶可以移動迷宮來引導彈球穿過迷宮。現在我們專注於讓用戶做到這些。當執行模擬器時,遊戲使用設備和鍵盤上的加速器輸入來引導彈球穿過迷宮。

了解加速器輸入 (Accelerometer Input)

當讀到加速器輸入的時候,我們應該意識到這與以前描述的繪圖坐標軸系統的主要不同是:加速器坐標軸會以設備本身為主,這和繪圖坐標軸與方向無關。

請看下面的圖例,說明了當傾斜設備時,加速器 X 軸的值是如何以 Y 軸為中心變化的:


圖例24
透過傾斜設備來改變加速器 X 軸的值

正如你在圖例中看到的一樣,在 Y 軸上順時針旋轉手機會增加 X 軸的值;逆時針傾斜則會減小 X 的值。此外,注意不管設備的方向如何,從手機按鈕到另一邊,加速器的 Y 軸保持在同一軸線上!

下面的圖例展示了當在 X 軸上傾斜手機,加速器輸入的變化:


圖例 25
透過傾斜設備改變加速器 X 軸的值

加速器 Z 軸的值有點不同。因為 Z 軸的變化時透過旋轉一個坐標軸完成的,而不是沿著坐標軸移動。

像下面圖例演示的一樣,當手機從地面向上抬起的時候加速器會返回 Z 軸的負值;當手機向下移動時,Z 為正值:


圖例 26
當整個手機下降時,加速器返回 Z 軸的正值

以某個特定角度傾斜手機時,加速器X和Y軸的值保持不變 (例如,45 度向右傾斜手機加速器 X 軸的值一直是 0.5。另一方面 Z 軸代表了實際的運動而不是手機當前的高度!正 / 負 Z 值各自代表了手機以一個特定的速度下移 / 上移並且在當手機在某個高度不動,返回值一直是 0。

1. 在 “MarbleMazeGame” 專案中建立一個新的專案資料夾。

2. 瀏覽到實驗檔案安裝資料夾,然後找到 Assets \ Code \ Misc。把這個目錄下的 “Accelerometer.cs” 檔添加到以前步驟中建立的專案資料夾裡。這些程式碼資源提供了與設備加速器的輕鬆互動。當在模擬器中執行時,它還可以用鍵盤輸入替換實際的加速器輸入。

**說明:**這個資源基於 Creators Club 的 “Accelerometer” 範例。你可以從下面的位址中獲得完整的範例 https://creators.xna.com/en-US/sample/accelerometer

3. 在 “Screens” 專案資料夾下打開“GameplayScreen.cs” 文件並向 GameplayScreen 類別中添加一些額外欄位:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 3 – GameplayScreen Accelerometer Fields)

C#

readonly float angularVelocity = MathHelper.ToRadians(1.5f);
 
Vector3? accelerometerState = Vector3.Zero;

我們將使用上述欄位和設備內置的加速器進行互動。

4. 瀏覽到GameplayScreen 類別的 LoadContent 方法,按照下面程式碼修改 (灰色的是舊程式碼):

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 4 – GameplayScreen Call to Initialize)

C#

public override void LoadContent()
{
    LoadAssets();
 
    Accelerometer.Initialize();
 
    base.LoadContent();
}

你可能想知道為什麼在內容載入階段而不是初始化階段來初始化加速器。原因是建議盡可能將初始化加速器的程序往後延,更確切的說是呼叫 “Start” 方法。在更新 / 繪製週期開始之前,內容載入階段是最後的階段,所以我們在這進行初始化。我們原本應該在第一個更新週期中做初始化,但是這就意味著添加不必要的條件陳述式來更新遊戲主迴圈。

5. 向 MarbleMazeGame 專案添加一個 Microsoft.Phone 類別庫的引用。你將在下面的步驟中需要這個引用。

6. 為了添加一個引用,在 “Solution Explorer” 裡的 MarbleMazeGame,右鍵點擊 “References” 節點並從快顯功能表中選擇“Add Reference”:


圖例 27
向專案裡添加一個引用

7. 在打開的 “Add Reference” 視窗,找到並選擇 “Microsoft.Phone” 類別庫然後點擊 “Ok”


圖例 28
向專案中添加一個引用

8. 添加另一個引用 - Microsoft.Devices.Sensors 類別庫。

9. 在 “GameplayScreen.cs” 文件開頭添加如下宣告:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 9 – GameplayScreen Using Statement)

C#

using Microsoft.Devices;

10. 接下來,覆寫 HandleInput 方法,使 gamesplay screen 對用戶輸入做出反應:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 10 – GameplayScreen HandleInput)

C#

public override void HandleInput(InputState input)
{
    if (input == null)
        throw new ArgumentNullException("input");

    // Rotate the maze according to accelerometer data
    Vector3 currentAccelerometerState = Accelerometer.GetState().Acceleration;

    if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device)
    {
        //Change the velocity according to acceleration reading
        maze.Rotation.Z = (float)Math.Round(MathHelper.ToRadians(currentAccelerometerState.Y * 30), 2);
        maze.Rotation.X = -(float)Math.Round(MathHelper.ToRadians(currentAccelerometerState.X * 30), 2);
    }
    else if (Microsoft.Devices.Environment.DeviceType == DeviceType.Emulator)
    {
        Vector3 Rotation = Vector3.Zero;

        if (currentAccelerometerState.X != 0)
        {
            if (currentAccelerometerState.X > 0)
                Rotation += new Vector3(0, 0, -angularVelocity);
            else
                Rotation += new Vector3(0, 0, angularVelocity);
        }

        if (currentAccelerometerState.Y != 0)
        {
            if (currentAccelerometerState.Y > 0)
                Rotation += new Vector3(-angularVelocity, 0, 0);
            else
                Rotation += new Vector3(angularVelocity, 0, 0);
        }

        // Limit the rotation of the maze to 30 degrees
        maze.Rotation.X =
            MathHelper.Clamp(maze.Rotation.X + Rotation.X,
            MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

        maze.Rotation.Z =
            MathHelper.Clamp(maze.Rotation.Z + Rotation.Z,
            MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

    }
}

上面的方法雖然長,但是卻很簡單。我們檢查現在使用的是模擬器還是真實的設備並在這兩種情況下對加速器類別輸入做不同的處理,因為當使用模擬器加速器時,資料是鍵盤產生的。我們還要確保在上述兩種情況中把彈球滾動的角度限制在 30 度以內。

11. 編譯並部署遊戲。你應該能夠用鍵盤或加速器輸入來旋轉繪製的元素。

說明:如果模擬器對鍵盤輸入沒反應,當焦點在模擬器上的時候,按下鍵盤上的 pause


圖例 29
執行一定旋轉後的遊戲畫面

現在我們可以旋轉彈球了,接下來修改 camera,這樣彈球就可以沿迷宮走了。當球滾動透過迷宮時,這個動作非常有用。

12. 打開位於 Objects 專案資料夾下的Camera.cs 檔並修改 Camera 類別的欄位。因為我們不在需要固定的 camera,我們可以修改 “position” 和 “target” 欄位並添加一些額外的欄位。最終,類別裡應該包括下列欄位 (以前顯示過的和不變的欄位都改成了灰色):

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 12 – Camera Position Fields)

C#

private Vector3 position = Vector3.Zero;
private Vector3 target = Vector3.Zero;
private GraphicsDevice graphicsDevice;
 
public Vector3 ObjectToFollow { get; set; }
public Matrix Projection { get; set; }
public Matrix View { get; set; }
 
private readonly Vector3 cameraPositionOffset = new Vector3(0, 450, 100);
private readonly Vector3 cameraTargetOffset = new Vector3(0, 0, -50);

13. 修改Camera 類別的 Initialize 覆寫方法,因為現在的畫面會不斷的沿著迷宮,所以我們不再使用它設定 camera 的畫面 (並且“Initialize” 只發生一次):

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 13 – Camera Initialize)

C#

public override void Initialize()
{
    // Create the projection matrix
    Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(50), graphicsDevice.Viewport.AspectRatio, 1, 10000);
 
    base.Initialize();
}

14. 覆寫 Update 方法。在這個方法裡我們要確保 camera 是沿著迷宮走的:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 14 – Camera Update)

C#

public override void Update(GameTime gameTime)
{
 
    // Make the camera follow the object
    position = ObjectToFollow + cameraPositionOffset;
 
    target = ObjectToFollow + cameraTargetOffset;
 
    // Create the view matrix
    View = Matrix.CreateLookAt(position, target, Vector3.Up);
 
    base.Update(gameTime);
}

上面的程式碼把 camera 設定在與 “ObjectToFollow” 相關的位置上並指向了一個與 “ObjectToFollow” 相關的目標位置。為了確保 “ObjectToFollow” 的設定一直都是合適的,我們需要修改 Marble 類別。

15. 從 Objects 專案資料夾中打開Marble.cs 檔並向 Marble 類別中添加如下屬性:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 15 – Marble Maze Property)

C#

public Maze Maze { get; set; }

16. 在 Marble 類別中覆寫 Update 方法:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 16 – Marble Update)

C#

public override void Update(GameTime gameTime)
{
    base.Update(gameTime);
 
    // Make the camera follow the marble
    Camera.ObjectToFollow = Vector3.Transform(Position,
        Matrix.CreateFromYawPitchRoll(Maze.Rotation.Y,
        Maze.Rotation.X, Maze.Rotation.Z));
}

17. 最後的步驟將把迷宮和彈球關聯起來。在 Screens 專案資料夾下打開 GameplayScreen.cs 檔並修改GameplayScreen 類別的 InitializeMarble 方法如下:

(Code Snippet – 3D Game Development – Ex 1 Task 3 Step 17 – GameplayScreen InitializeMarble)

C#

private void InitializeMarble()
{
    marble = new Marble(ScreenManager.Game as MarbleMazeGame)
    {
        Position = new Vector3(100, 0, 0),
        Camera = camera,
        Maze = maze
    };
 
    marble.Initialize();
}

18. 編譯並部署你的專案。現在 camera 應該跟隨著彈球。Camera 的行為看起來有些奇怪,但是這是因為彈球在空間裡活動而不是和迷宮交互。我們將在下面的任務中進行修正。


圖例30
camera 跟隨彈球


任務 4 – 物理學演算法和碰撞偵測與處理

雖然在以前的任務裡,我們使遊戲可以進行交互,但是它還是不好玩,因為用戶可以交換的只是遊戲的 camera。本任務的目的是透過添加物理學和碰撞檢測使遊戲變得完全可玩,這樣用戶就可以引導彈球穿過迷宮了。

我們將從建立一個自訂的內容處理器開始,這個處理器會給迷宮模型增加更多的資訊來説明我們實現彈球和迷宮的碰撞檢測。

1. 向解決方案中添加一個內容管道擴充專案。在 solution explorer 裡右鍵點擊解決方案並選擇 Add | New Project


圖例31
向解決方案裡添加一個新專案

2. 在彈出的視窗裡選擇“Content Pipeline Extension Library” 專案並命名為 MarbleMazePipeline


圖例32
建立一個內容管道擴充專案

3. 新建立的專案將包括一個名為 “ContentProcessor1.cs” 的程式碼檔。修改檔案名為 “MarbleMazeProcessor.cs”。

4. 打開 MarbleMazeProcessor.cs 檔並刪除所有檔內容。

5. 在文件開頭添加如下宣告:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 5 – MarbleMazeProcessor Using Statements)

C#

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

6. 在 “MarbleMazeProcessor.cs” 檔裡添加如下命名空間和類別定義。添加的類別將用於內容處理器的實作:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 6 – MarbleMazeProcessor Class Definition)

C#

namespace MarbleMazePipeline
{ 
    [ContentProcessor]
    public class MarbleMazeProcessor : ModelProcessor
    {
    }
}

這個內容處理器將附上一個模型的 “Tag” 屬性,一個把模型網格與對應網格裡定義的頂點集合進行匹配的字典。我們將在後面使用這個資訊來計算碰撞。

7. 在新的 MarbleMazeProcessor 類別中添加如下欄位。這個欄位用於保存當模型經過處理器時,我們希望附在上面的資訊:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 7 – MarbleMazeProcessor tagData Field)

C#

Dictionary<string, List<Vector3>> tagData = new Dictionary<string, List<Vector3>>();

8. 向 MableMazeProcessor 類別添加一個輔助類別:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 8 – MarbleMazeProcessor FindVertices)

C#

void FindVertices(NodeContent node)
{
    // Is this node a mesh?
    MeshContent mesh = node as MeshContent;
 
    if (mesh != null)
    {
        string meshName = mesh.Name;
        List<Vector3> meshVertexs = new List<Vector3>();
        // Look up the absolute transform of the mesh.
        Matrix absoluteTransform = mesh.AbsoluteTransform;
        // Loop over all the pieces of geometry in the mesh.
        foreach (GeometryContent geometry in mesh.Geometry)
        {
            // Loop over all the indices in this piece of geometry.
            // Every group of three indices represents one triangle.
            foreach (int index in geometry.Indices)
            {
                // Look up the position of this vertex.
                Vector3 vertex = geometry.Vertices.Positions[index];
 
                // Transform from local into world space.
                vertex = Vector3.Transform(vertex, absoluteTransform);
 
                // Store this vertex.
                meshVertexs.Add(vertex);
            }
        }

        tagData.Add(meshName, meshVertexs);
    }
 
    // Recursively scan over the children of this node.
    foreach (NodeContent child in node.Children)
    {
        FindVertices(child);
    }
}

上面的方法只是遞迴的掃描了一個模型並建立步驟 7 裡提到的字典。注意像程式碼注釋裡說的一樣,我們要確保每 3 頂點的連續集合定義一個屬於網格的三角。

9. 最後,我們將覆寫 Process 方法來使用我們剛剛定義的輔助方法執行我們自訂的內容處理器:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 9 – MarbleMazeProcessor Process)

C#

public override ModelContent Process(NodeContent input, ContentProcessorContext context)
{
    FindVertices(input);
 
    ModelContent model = base.Process(input, context);
 
    model.Tag = tagData;
 
    return model;
}

10. 我們希望迷宮模型使用新的自訂的處理器。在 “MarbleMazeGameContent” 內容專案中的 “MarbleMazePipeline” 專案裡添加一個引用。

11. 編譯你的專案。

12. 在內容專案的 ”Models” 資料夾下,右鍵點擊迷宮模型檔“maze1.FBX”,並選擇 Properties

13. 在 properties window 中,選擇 “MarbleMazePipeline” 作為內容處理器:


圖例 33
在迷宮模型上的內容處理器屬性


圖例 34
選擇 “MarbleMazeProcessor” 作為內容處理器

14. 現在是向遊戲裡添加物理學演算法的時候了。最終的遊戲實體是由遊戲邏輯構成的,控制彈球滾動透過迷宮的方式,碰撞牆壁或掉到迷宮的洞裡。我們將從添加程式碼資源開始,這些程式碼將用於以後的碰撞檢測。

15. 瀏覽到實驗安裝資料夾,然後找到Assets \ Code \ Misc。把這個目錄下的 “TriangleSphereCollisionDetection.cs” 檔拷貝到 “Misc” 專案資料夾中。

16. 透過右鍵點擊 “Misc” 專案資料夾並選擇 Add | New Item 來添加一個類別檔:


圖例 35
向專案資料夾裡添加一個新條目

17. 在出現的對話方塊裡,選擇 Code File 並將其命名為 IntersectDetails.cs


圖例 36
向專案裡添加一個新的類別檔

18. 打開新建立的類別檔,它應該是空的,把下面的程式碼塊填充到此文件中:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 18 – IntersectDetails Class)

C#

using System;
using System.Collections.Generic
 
namespace MarbleMazeGame
{
    public struct IntersectDetails
    {
        public bool IntersectWithGround;
        public bool IntersectWithFloorSides;
        public bool IntersectWithWalls;
 
        public Triangle IntersectedGroundTriangle;
        public IEnumerable<Triangle> IntersectedFloorSidesTriangle;
        public IEnumerable<Triangle> IntersectedWallTriangle; 
    }
}

上面定義的結構將會用於保存碰撞資訊,也就是發生的碰撞和發生碰撞的迷宮部分。

  • 我們把視線轉移到更新 DrawableComponent3D 類別上,它將作為受物理學影響的所有實體的父類別。

19. 從 “Objects” 專案資料夾下打開 “DrawableComponent3D.cs” 檔,並將下列枚舉定義添加到 DrawableComponent3D 類別定義之前:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 19 – DrawableComponent3D Axis Enum)

C#

[Flags]
public enum Axis
{
    X = 0x1,
    Y = 0x2,
    Z = 0x4
}

20. 透過添加額外的欄位來修改 DrawableComponent3D 類別的欄位定義。這個類別應該包括下列欄位 (我們已經把已有的欄位置成灰色):

C#

public const float gravity = 100 * 9.81f;
public const float wallFriction = 100 * 0.8f;

string modelName;
protected bool preferPerPixelLighting = false;
public Model Model = null;
protected IntersectDetails intersectDetails = new IntersectDetails();
protected float staticGroundFriction = 0.1f;

public Vector3 Position = Vector3.Zero;
public Vector3 Rotation = Vector3.Zero;
public Vector3 Velocity = Vector3.Zero;
public Vector3 Acceleration = Vector3.Zero;

public Matrix[] AbsoluteBoneTransforms;
public Matrix FinalWorldTransforms;
public Matrix OriginalWorldTransforms = Matrix.Identity;
public Camera Camera;

注意 “FinalWorldTransform” 欄位,它已經被反白顯示。因為它已經定義過了,我們不再把它設定到同一個matrix。

21. 修改DrawableComponent3D 類別的 Update 覆寫方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 21 – DrawableComponent3D Physics Calculations)

C#

public override void Update(GameTime gameTime)
{
    // Perform physics calculations
    CalcPhysics(gameTime);
 
    // Update the final transformation to properly place the component in the
    // game world.
    UpdateFinalWorldTransform();
 
    base.Update(gameTime);
}

你可以看到,唯一一處修改是我們更新了元件物理演算法,將其作為更新階段的一部分。

22. 向類別中添加上面步驟提到的方法 - CalcPhysics

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 22 – DrawableComponent3D CalcPhysics)

C#

protected virtual void CalcPhysics(GameTime gameTime)
{
    CalculateCollisions();
    CalculateAcceleration();
    CalculateFriction();
    CalculateVelocityAndPosition(gameTime);
}

上面的方法指向了一個物理計算的特定順序。我們將先計算碰撞,加速度和摩擦,然後根據以往的資料計算元件的實際速度和位置。我們現在來實現上述四個方法,它們繼承的類別是可以被覆寫的以防他們需要的計算和預設的不同。

23. 因為普通的元件不能以特殊的方式移動,我們將設定上述方法為抽象方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 23 – DrawableComponent3D Abstract Methods)

C#

protected abstract void CalculateFriction();
 
protected abstract void CalculateAcceleration();        

 
protected abstract void CalculateVelocityAndPosition(GameTime gameTime);
 
protected abstract void CalculateCollisions();

到這,我們完成了對 DrawableComponent3D  類別的處理。下面我們將轉移到 Maze 類別。

24. 在 “Objects” 專案資料夾下打開“Maze.cs” 文件,向 Maze 類別中添加下列欄位:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 24 – Maze Additional Fields)

C#

public List<Vector3> Ground = new List<Vector3>();
public List<Vector3> Walls = new List<Vector3>();
public List<Vector3> FloorSides = new List<Vector3>();
public LinkedList<Vector3> Checkpoints = new LinkedList<Vector3>();
public Vector3 StartPoistion;
public Vector3 End;

前三個欄位將用於保存地面,牆壁和 “floor sides” (牆壁的凹處) 頂點。跟以前添加自訂內容處理時討論過的一樣,把它們排列成三角形。我們將使用那個自訂內容處理器提供的資料來填充這些欄位。

25. 在 Maze 類別裡覆寫 LoadContent 方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 25 – MazeLoadContent)

C#

protected override void LoadContent()
{
    base.LoadContent();

    // Load the start & end positions of the maze from the bone
    StartPoistion = Model.Bones["Start"].Transform.Translation;
    End = Model.Bones["Finish"].Transform.Translation;            

    // Get the maze's triangles from its mesh
    Dictionary<string, List<Vector3>> tagData = 
        (Dictionary<string, List<Vector3>>)Model.Tag;

    Ground = tagData["Floor"];
    FloorSides = tagData["floorSides"];

    Walls = tagData["walls"];

    // Add checkpoints to the maze
    Checkpoints.AddFirst(StartPoistion);
    foreach (var bone in Model.Bones)
    {
        if (bone.Name.Contains("spawn"))
        {
            Checkpoints.AddLast(bone.Transform.Translation);
        }
    }
}

上面的方法相當簡單,它只是將我們自訂內容處理器添加到模型裡的資料填充到這些欄位裡。程式碼的最後部分定義了檢查點,如果彈球滾動經過,它就會啟動。在掉進坑裡之後,彈球將會在這復活。

26. 因為迷宮本身不會受物理的任何影響,我們將只是在添加裡下列空方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 26 – Maze Physics)

C#

protected override void CalculateCollisions()
{
    // Nothing to do - Maze doesn't collide with itself
}
 
protected override void CalculateVelocityAndPosition(GameTime gameTime)
{
    // Nothing to do - Maze doesn't move
}
 
protected override void CalculateFriction()
{
    // Nothing to do - Maze is not affected by friction
}
 
protected override void CalculateAcceleration()
{
    // Nothing to do - Maze doesn't move
}

27. 最後,我們將在 Maze  類別中添加一個名為 GetCollisionDetails 的方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 27 – Maze GetCollisionDetails)

C#

public void GetCollisionDetails(BoundingSphere BoundingSphere, ref IntersectDetails intersectDetailes, bool light)
{
    intersectDetailes.IntersectWithGround =
        TriangleSphereCollisionDetection.IsSphereCollideWithTringles(Ground,
        BoundingSphere, out intersectDetailes.IntersectedGroundTriangle, 
        true);
    intersectDetailes.IntersectWithWalls =
        TriangleSphereCollisionDetection.IsSphereCollideWithTringles(Walls,
        BoundingSphere, out intersectDetailes.IntersectedWallTriangle, light);
    intersectDetailes.IntersectWithFloorSides =
        TriangleSphereCollisionDetection.IsSphereCollideWithTringles(
        FloorSides, BoundingSphere,         out 
ntersectDetailes.IntersectedFloorSidesTriangle, 
        true);
}

這個方法允許你給迷宮一個包容球 (bounding sphere),回溯到 intersection 的細節,它將告訴我們迷宮的那部分發生了球體碰撞。程式碼本身使用的資源是我們在步驟 15 中添加的。

現在我們改進了 Marble 類別。當我們即將實作它的物理演算法時,需要做更多的工作。

28. 在 “Objects” 專案資料夾下打開“Marble.cs” 檔並將下列宣告添加到檔中:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 28 – Marble Using Statement)

C#

using System.Collections.Generic;

29. 在 Marble 類別裡添加下列欄位定義:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 29 – Marble Additional Fields)

C#

Matrix rollMatrix = Matrix.Identity;
Vector3 normal;
public float angleX;
public float angleZ;

30. 在 Marble 類別裡添加下列屬性:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 30 – Marble BoundingSphereTransformed)

C#

public BoundingSphere BoundingSphereTransformed
{
    get
    {
        BoundingSphere boundingSphere = Model.Meshes[0].BoundingSphere;
        boundingSphere = boundingSphere.Transform(AbsoluteBoneTransforms[0]);
        boundingSphere.Center += Position;
        return boundingSphere;
    }
}

這個屬性將返回 3D 轉換的迷宮包容球,並且還要把包容球與遊戲世界中的迷宮匹配因素考慮在內。

31. 在 Marble  類別裡覆寫 DrawableComponent3D 類別的 UpdateFinalWorldTransform 方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 31 – Marble UpdateFinalWorldTransform)

C#

protected override void UpdateFinalWorldTransform()
{
    // Calculate the appropriate rotation matrix to represent the marble
    // rolling inside the maze
    rollMatrix *= Matrix.CreateFromAxisAngle(Vector3.Right, Rotation.Z) *
        Matrix.CreateFromAxisAngle(Vector3.Forward, Rotation.X);
 
    // Multiply by two matrices which will place the marble in its proper 
    // position and align it to the maze (which tilts due to user input)
    FinalWorldTransforms = rollMatrix *
                Matrix.CreateTranslation(Position) *
                Matrix.CreateFromYawPitchRoll(Maze.Rotation.Y, 
                Maze.Rotation.X, Maze.Rotation.Z); 
}

這個覆寫方法會使彈球模型根據它的 “Rotation” 值進行旋轉,這樣當它移動的時候看起來像滾動。

32. 覆寫 CalculateCollisions 方法。我們只簡單地把彈球的包容球傳給迷宮來執行碰撞就是並將結果保存到 Marble 類別的一個欄位中去。

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 32 – Marble CalculateCollisions)

C#

protected override void CalculateCollisions()
{
    Maze.GetCollisionDetails(BoundingSphereTransformed, ref intersectDetails,  false);

    if (intersectDetails.IntersectWithWalls)
    {
        foreach (var triangle in intersectDetails.IntersectedWallTriangle)
        {
            Axis direction = CollideDirection(triangle);
            if ((direction & Axis.X) == Axis.X && 
                (direction & Axis.Z) == Axis.Z)
            {
                Maze.GetCollisionDetails(BoundingSphereTransformed, 
                                         ref intersectDetails, true);
            }
        }
    }
}

33. 覆寫 CalculateAcceleration 方法。這個方法將根據迷宮的傾斜修改彈球的加速度:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 33 – Marble CalculateAcceleration)

C#

protected override void CalculateAcceleration()
{
    if (intersectDetails.IntersectWithGround)
    {
        // We must take both the maze's tilt and the angle of the floor
        // section beneath the marble into account
        angleX = 0;
        angleZ = 0;
        if (intersectDetails.IntersectedGroundTriangle != null)
        {
            intersectDetails.IntersectedGroundTriangle.Normal(out normal);
            angleX = (float)Math.Atan(normal.Y / normal.X);
            angleZ = (float)Math.Atan(normal.Y / normal.Z);

            if (angleX > 0)
            {
                angleX = MathHelper.PiOver2 - angleX;
            }
            else if (angleX < 0)
            {
                angleX = -(angleX + MathHelper.PiOver2);
            }

            if (angleZ > 0)
            {
                angleZ = MathHelper.PiOver2 - angleZ;
            }
            else if (angleZ < 0)
            {
                angleZ = -(angleZ + MathHelper.PiOver2);
            }
        }


        // Set the final X, Y and Z axis acceleration for the marble
        Acceleration.X = -gravity * (float)Math.Sin(Maze.Rotation.Z - angleX);
        Acceleration.Z = gravity * (float)Math.Sin(Maze.Rotation.X - angleZ);
        Acceleration.Y = 0;
    }
    else
    {
        // If the marble is not touching the floor, it is falling freely
        Acceleration.Y = -gravity;
    }


    if (intersectDetails.IntersectWithWalls)
    {
        // Change the marble's acceleration due to a collision with a maze
        // wall
        UpdateWallCollisionAcceleration(
            intersectDetails.IntersectedWallTriangle);
    }
    if (intersectDetails.IntersectWithFloorSides)
    {
        // Change the marble's acceleration due to collision with a pit wall
        UpdateWallCollisionAcceleration(
            intersectDetails.IntersectedFloorSidesTriangle);
    }
}

方法中唯一不平凡的地方是第一行注釋下面的部分,它只負責計算地面自己的角度來影響彈球當前處在的整個斜面。在方法後面,我們使用了一個説明方法來根據碰撞更新加速度。我們將實現這個輔助方法,但是首先我們要引入另一個方法。

34. 在 Marble 類別中添加下列方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 34 – Marble CollideDirection)

C#

protected Axis CollideDirection(Triangle collideTriangle)
{
    if (collideTriangle.A.Z == collideTriangle.B.Z && 
        collideTriangle.B.Z == collideTriangle.C.Z)
    {
        return Axis.Z;
    }
    else if (collideTriangle.A.X == collideTriangle.B.X && 
        collideTriangle.B.X == collideTriangle.C.X)
    {
        return Axis.X;
    }
    else if (collideTriangle.A.Y == collideTriangle.B.Y && 
        collideTriangle.B.Y == collideTriangle.C.Y)
    {
        return Axis.Y;
    }
    return Axis.X | Axis.Z;
}

這個方法就是檢測三角點來決定它上面的平面並返回垂直此平面的座標值。

35. 現在在 Marble 類別裡添加如下方法,用於根據碰撞修改加速度:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 35 – Marble UpdateWallCollisionAcceleration)

C#

protected void UpdateWallCollisionAcceleration(IEnumerable<Triangle> wallTriangles)
{
    foreach (var triangle in wallTriangles)
    {
        Axis direction = CollideDirection(triangle);
        // Decrease the acceleration in x-axis of the component
        if ((direction & Axis.X) == Axis.X)
        {
            if (Velocity.X > 0)
                Acceleration.X -= wallFriction;
            else if (Velocity.X < 0)
                Acceleration.X += wallFriction;
        }
 
        // Decrease the acceleration in z-axis of the component
        if ((direction & Axis.Z) == Axis.Z)
        {
            if (Velocity.Z > 0)
                Acceleration.Z -= wallFriction;
            else if (Velocity.Z < 0)
                Acceleration.Z += wallFriction;
        }      
    }
}

這個方法只是給彈球提供了一個加速度元件,當碰到牆壁時把方向反過來。

36. 覆寫 CalculateFriction 方法向 Marble 類別中引入摩擦計算:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 34 – Marble CalculateFriction)

C#

protected override void CalculateFriction()
{
    if (intersectDetails.IntersectWithGround)
    {
        if (Velocity.X > 0)
        {
            Acceleration.X -= staticGroundFriction * gravity *
                (float)Math.Cos(Maze.Rotation.Z - angleX);
        }
        else if (Velocity.X < 0)
        {
            Acceleration.X += staticGroundFriction * gravity *
                (float)Math.Cos(Maze.Rotation.Z - angleX);
        }
 
        if (Velocity.Z > 0)
        {
            Acceleration.Z -= staticGroundFriction * gravity *
                (float)Math.Cos(Maze.Rotation.X - angleZ);
        }
        else if (Velocity.Z < 0)
        {
            Acceleration.Z += staticGroundFriction * gravity *
                (float)Math.Cos(Maze.Rotation.X - angleZ);
        }
 
    }
}

上面的方法添加了一個加速度元件,這個加速度和彈球當前的速度相反並且和彈球所在的坡度成比例。

37. 最後要做的事情是為了完全支援彈球的物理演算法,即透過覆寫 CalculateVelocityAndPosition 來更新他的速度和位置:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 37 – Marble CalculateVelocityAndPosition)

C#

protected override void CalculateVelocityAndPosition(GameTime gameTime)
{
    // Calculate the current velocity
    float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
 
    Vector3 currentVelocity = Velocity;
 
    Velocity = currentVelocity + (Acceleration * elapsed);
 
    // Set a bound on the marble's velocity
    Velocity.X = MathHelper.Clamp(Velocity.X, -250, 250);
    Velocity.Z = MathHelper.Clamp(Velocity.Z, -250, 250);
 
    if (intersectDetails.IntersectWithGround)
    {
        Velocity.Y = 0;
    }
 
    if (intersectDetails.IntersectWithWalls)
    {
        UpdateWallCollisionVelocity(
            intersectDetails.IntersectedWallTriangle, ref currentVelocity);
    }
 
    if (intersectDetails.IntersectWithFloorSides)
    {
        UpdateWallCollisionVelocity(
            intersectDetails.IntersectedFloorSidesTriangle,
            ref currentVelocity);
    }
 
    // If the velocity is low, simply cause the marble to halt
    if (-1 < Velocity.X && Velocity.X < 1)
    {
        Velocity.X = 0;
    }
    if (-1 < Velocity.Z && Velocity.Z < 1)
    {
        Velocity.Z = 0;
    }
 
    // Update the marble's position
    UpdateMovement((Velocity + currentVelocity) / 2, elapsed);
}

上面的方法根據計算的加速的和連續叫用方法的時間間隔來改變彈球的速度。這個方法還使用了我們即將實現的輔助方法,如果發生碰撞,它會改變了彈球的速度,而且如果速度突然下降,它還會使彈球停下來。這個輔助方法還更新了彈球的位置,我們稍後實作它。

38. 在 Marble 類別添加如下方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 38 – Marble UpdateWallCollisionVelocity)

C#

protected void UpdateWallCollisionVelocity(IEnumerable<Triangle> wallTriangles, ref Vector3 currentVelocity)
{
    foreach (var triangle in wallTriangles)
    {
        Axis direction = CollideDirection(triangle);
        // Swap the velocity between x & z if the wall is diagonal
        if ((direction & Axis.X) == Axis.X && (direction & Axis.Z) == Axis.Z)
        {
            float tmp = Velocity.X;
            Velocity.X = Velocity.Z;
            Velocity.Z = tmp;
 
            tmp = currentVelocity.X;
            currentVelocity.X = currentVelocity.Z * 0.3f;
            currentVelocity.Z = tmp * 0.3f;
        }
        // Change the direction of the velocity in the x-axis
        else if ((direction & Axis.X) == Axis.X)
        {
            if ((Position.X > triangle.A.X && Velocity.X < 0) ||
                (Position.X < triangle.A.X && Velocity.X > 0))
            {
                Velocity.X = -Velocity.X * 0.3f;
                currentVelocity.X = -currentVelocity.X * 0.3f;
            }
        }
        // Change the direction of the velocity in the z-axis
        else if ((direction & Axis.Z) == Axis.Z)
        {
            if ((Position.Z > triangle.A.Z && Velocity.Z < 0) ||
                (Position.Z < triangle.A.Z && Velocity.Z > 0))
            {
                Velocity.Z = -Velocity.Z * 0.3f;
                currentVelocity.Z = -currentVelocity.Z * 0.3f;
            }
        }
    }
}

上面的方法只是當彈球撞擊平面牆壁時反轉並降低它的速度。但是如果牆是對角的,彈球的速度會在 X 軸和 Z 軸之間轉換,這是基於迷宮只有 45 度的對角牆假設上的。

39. 在 Marble 類別中加入最後一個物理相關演算法的方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 39 – Marble UpdateMovement)

C#

private void UpdateMovement(Vector3 deltaVelocity, float deltaTime)
{
    // Calculate the change in the marble's position
    Vector3 deltaPosition = deltaVelocity * deltaTime;
 
    // Before setting the new position, we must make sure it is legal
    BoundingSphere nextPosition = this.BoundingSphereTransformed;
    nextPosition.Center += deltaPosition;
    IntersectDetails nextIntersectDetails = new IntersectDetails();
    Maze.GetCollisionDetails(nextPosition, ref nextIntersectDetails, true);
    nextPosition.Radius += 1.0f;
 
    // Move the marble
    Position += deltaPosition;
 
    // If the floor not straight then we must reposition the marble vertically
    Vector3 forwardVecX = Vector3.Transform(normal,
       Matrix.CreateRotationZ(-MathHelper.PiOver2));
 
    Vector3 forwardVecZ = Vector3.Transform(normal,
        Matrix.CreateRotationX(-MathHelper.PiOver2));
 
    bool isGroundStraight = true;
    if (forwardVecX.X != -1 && forwardVecX.X != 0)
    {
       Position.Y += deltaPosition.X / forwardVecX.X * forwardVecX.Y;
        isGroundStraight = false;
    }
    if (forwardVecZ.X != -1 && forwardVecZ.X != 0)
    {
        Position.Y += deltaPosition.Z / forwardVecZ.Z * forwardVecZ.Y;
        isGroundStraight = false;
    }
    // If the marble is already inside the floor, we must reposition it
    if (isGroundStraight && nextIntersectDetails.IntersectWithGround)
    {
        Position.Y = nextIntersectDetails.IntersectedGroundTriangle.A.Y +
            BoundingSphereTransformed.Radius;
    } 
    // Finally, we "roll" the marble in accordance to its movement
    if (BoundingSphereTransformed.Radius != 0)
    {
        Rotation.Z = deltaPosition.Z / BoundingSphereTransformed.Radius;
        Rotation.X = deltaPosition.X / BoundingSphereTransformed.Radius;
    }
}

上面的方法只是根據當前的速度重設了彈球,但是然後修正了它的位置來避免它穿過牆壁或地面的對角部分,因為彈球必須上下移動來適應迷宮地面的斜面。

40. 在 GameplayScreen 類別中添加下列欄位:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 40 – GameplayScreen Additional Fields)

C#

bool gameOver = false;
LinkedListNode<Vector3> lastCheackpointNode;
SpriteFont timeFont;
TimeSpan gameTime;

我們使用上述欄位來追蹤遊戲流程。我們將在以後會看到他們是如何發揮作用的。

41. 瀏覽到 LoadContent 方法並按照下面進行修改:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 41 – GameplayScreen Load Fonts)

C#

public override void LoadContent()
{
    LoadAssets();
 
    timeFont = ScreenManager.Game.Content.Load<SpriteFont>(@"Fonts\MenuFont");
 
    Accelerometer.Initialize();
 
    base.LoadContent();
}

42. 瀏覽到 InitializeMaze 方法,並添加一些程式碼來初始化第一個檢查點,它實際上是迷宮的起始點:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 42 – GameplayScreen Save Checkpoint)

C#

private void InitializeMaze()
{
    maze = new Maze(ScreenManager.Game as MarbleMazeGame)
    {
        Position = Vector3.Zero,
        Camera = camera
    };
 
    maze.Initialize();
 
    // Save the last checkpoint
    lastCheackpointNode = maze.Checkpoints.First;
}

43. 修改 GameplayScreen 類別的 InitializeMarble 方法來把彈球的初始位置設成迷宮的起始點:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 43 – GameplayScreen StartPosition)

C#

private void InitializeMarble()
{
    marble = new Marble(ScreenManager.Game as MarbleMazeGame)
    {
        Position = maze.StartPoistion,
        Camera = camera,
        Maze = maze
    };
 
    marble.Initialize();
}

44. 按照下面的實現修改 HandleInput 方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 44 – GameplayScreen Updated HandleInput)

C#

public override void HandleInput(InputState input)
{
    if (input == null)
        throw new ArgumentNullException("input");

    // Rotate the maze according to accelerometer data
    Vector3 currentAccelerometerState = Accelerometer.GetState().Acceleration;

    if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device)
    {
        //Change the velocity according to acceleration reading
        maze.Rotation.Z = (float)Math.Round(MathHelper.ToRadians(currentAccelerometerState.Y * 30), 2);
        maze.Rotation.X = -(float)Math.Round(MathHelper.ToRadians(currentAccelerometerState.X * 30), 2);
    }
    else if (Microsoft.Devices.Environment.DeviceType == DeviceType.Emulator)
    {
        Vector3 Rotation = Vector3.Zero;

        if (currentAccelerometerState.X != 0)
        {
            if (currentAccelerometerState.X > 0)
                Rotation += new Vector3(0, 0, -angularVelocity);
            else
                Rotation += new Vector3(0, 0, angularVelocity);
        }

        if (currentAccelerometerState.Y != 0)
        {
            if (currentAccelerometerState.Y > 0)
                Rotation += new Vector3(-angularVelocity, 0, 0);
            else
                Rotation += new Vector3(angularVelocity, 0, 0);
        }

        // Limit the rotation of the maze to 30 degrees
        maze.Rotation.X =
            MathHelper.Clamp(maze.Rotation.X + Rotation.X,
            MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

        maze.Rotation.Z =
            MathHelper.Clamp(maze.Rotation.Z + Rotation.Z,
            MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

    }
}

主要的修改是如果遊戲停止了就不在處理用戶輸入,還有就是加速器輸入受校準向量 (Calibration Vector) 的影響。我們將在下個練習中介紹校準向量的目的。此外,如果遊戲結束,tap 方法將會使用一個即將實現的輔助方法來初始化遊戲結束順序。

45. 修改 Update 方法來執行遊戲狀態的額外檢查:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 45 – GameplayScreen Update)

C#

public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
    // Calculate the time from the start of the game
    this.gameTime += gameTime.ElapsedGameTime;

    CheckFallInPit();
    UpdateLastCheackpoint();

    // Update all the component of the game
    maze.Update(gameTime);
    marble.Update(gameTime);
    camera.Update(gameTime);

    CheckGameFinish();

    base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
}

除了跟蹤整個遊戲時間,我們還是用即將實現的輔助類別來檢查彈球是否掉進洞裡,更新最後經過的檢查點並查看遊戲是否結束。一個新增的輔助方法負責處理遊戲結束的順序。

46. 向 GameplayScreen 類別中添加如下方法:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 46 – GameplayScreen UpdateLastCheckpoint)

C#

private void UpdateLastCheackpoint()
{
    BoundingSphere marblePosition = marble.BoundingSphereTransformed;
 
    var tmp = lastCheackpointNode;
    while (tmp.Next != null)
    {
        // If the marble is close to a checkpoint save the checkpoint
        if (Math.Abs(Vector3.Distance(marblePosition.Center, tmp.Next.Value))
            <= marblePosition.Radius * 3)
        {
            lastCheackpointNode = tmp.Next;
            return;
        }
        tmp = tmp.Next;
    }
}

上面的程式碼檢查迷宮裡所有比當前位置遠的檢查點。如果彈球離其中之一很近,它就被設定成當前的檢查點

47. 添加一個方法來檢查彈球是否掉進洞裡:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 47 – GameplayScreen CheckFallInPit)

C#

private void CheckFallInPit()
{
    if (marble.Position.Y < -150)
    {
        marble.Position = lastCheackpointNode.Value;
        maze.Rotation = Vector3.Zero;
        marble.Acceleration = Vector3.Zero;
        marble.Velocity = Vector3.Zero;
    }
}

一旦彈球掉進洞裡,上述程式碼重新就會把彈球的位置重新設定到最近的檢查點上。

48. 添加一個方法來檢查遊戲是否結束。它只會發生在玩家到達迷宮終點的時候:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 48 – GameplayScreen CheckGameFinish)

C#

private void CheckGameFinish()
{
    BoundingSphere marblePosition = marble.BoundingSphereTransformed;
 
    if (Math.Abs(Vector3.Distance(marblePosition.Center, maze.End)) <= 
        marblePosition.Radius * 3)
    {
        gameOver = true;
        return;
    }
}

49. 最後,我們將修改 Draw 方法來顯示整個使用的時間並且只在遊戲執行的時候工作。在遊戲的最終結果中,用時會作為玩家的分數,因為遊戲的目的就是用最少的時間闖過迷宮:

(Code Snippet – 3D Game Development – Ex 1 Task 4 Step 49 – GameplayScreen Draw)

C#

public override void Draw(GameTime gameTime)
{
    ScreenManager.GraphicsDevice.Clear(Color.Black);
    ScreenManager.SpriteBatch.Begin();

    // Draw the elapsed time
    ScreenManager.SpriteBatch.DrawString(timeFont,
        String.Format("{0:00}:{1:00}", this.gameTime.Minutes,
        this.gameTime.Seconds), new Vector2(20, 20), Color.YellowGreen);

    // Drawing sprites changes some render states around, which don't play
    // nicely with 3d models. 
    // In particular, we need to enable the depth buffer.
    DepthStencilState depthStensilState =
        new DepthStencilState() { DepthBufferEnable = true };
    ScreenManager.GraphicsDevice.DepthStencilState = depthStensilState;

    // Draw all the game components
    maze.Draw(gameTime);
    marble.Draw(gameTime);


    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

50. 編譯並部署遊戲。現在的遊戲雖然缺少些體驗性,但是完全可玩的了。目前,遊戲開始的很突然,玩家到達迷宮終點時遊戲結束的也很意外並且沒有聲音。在下面的練習中我們將透過添加功能表系統,遊戲結束時顯示的高分表和聲音重播來解決這些問題。我們還將引入校準螢幕,一旦遊戲部署到真實的設備上,校準螢幕會調整加速器。

練習 2:遊戲潤飾和功能表

在上面的練習中,我們實現了一個完全可玩的遊戲。但是,當我們在任務最後啟動遊戲,你會發現目前的狀態下,遊戲嚴重缺少潤飾。所以在本練習的第一個任務就是透過加入聲音來改進遊戲的外觀。

在後面的練習中,我們添加更多的元素作為遊戲的一部分,但並不是實際遊戲畫面的一部分。我們添加一個主功能表和操作指南畫面並使玩家可以暫停遊戲,還要顯示暫停畫面。此外,我們添加一個高分表來跟蹤遊戲中達到的最好成績。

我們還將添加一個校準螢幕,可以把任何方向設定成 “idle” 狀態,在這個狀態下迷宮是不傾斜的。

任務 1 – 聲音

1. 選擇 Misc 資料夾,並將實驗安裝資料夾下 Assets \ Code \ Misc 裡的 “AudioManager.cs” 文件添加進去。

說明:投石車大戰實驗中有 AudioManager 的詳細說明https://creators.xna.com/en-US/lab/catapultwars.

2. 現在,我們需要在內容專案中添加聲音資源。在 MarbleMazeGameContent” 專案裡添加一個新的專案資料夾,並命名為 “Sounds”,然後把實驗安裝資料夾下 Assets \ Media \ Sounds 裡的所有檔都添加到這個新資料夾下。

現在,我們將再次瀏覽以前練習中建立的各種類別,透過添加聲音重播 (audio playback) 來加強它們的功能。

3. 在我們可以播放聲音之前,我們需要初始化 AudioManager 並載入聲音。打開 “MarbleMazeGame.cs”  檔並加入下面程式碼以修改 MarbleMazeGame 類別的建構函式 (和以前一樣,舊程式碼被置成灰色):

(Code Snippet – 3D Game Development – Ex 2 Task 1 Step 3 – MarbleMazeGame Initialize AudioManager)

C#

AudioManager.Initialize(this);

4. 現在的建構函式如下所示:

(Code Snippet – 3D Game Development – Ex 2 Task 1 Step 4 – MarbleMazeGame Updated Constructor)

C#

public MarbleMazeGame()
{
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";

    // Frame rate is 30 fps by default for Windows Phone.
    TargetElapsedTime = TimeSpan.FromTicks(333333);
    //Create a new instance of the Screen Manager
    screenManager = new ScreenManager(this);
    Components.Add(screenManager);

    // Switch to full screen for best game experience
    graphics.IsFullScreen = true;

    graphics.SupportedOrientations =
       DisplayOrientation.LandscapeLeft;

    screenManager.AddScreen(new GameplayScreen(), null);

    // Initialize sound system
    AudioManager.Initialize(this);
}

5. 在 MarbleMazeGame 類別中添加下列功能 class:

(Code Snippet – 3D Game Development – Ex 2 Task 1 Step 5 – MarbleMazeGame LoadContent)

C#

protected override void LoadContent()
{
    AudioManager.LoadSounds();

    base.LoadContent();
}

它會使 AudioManager 載入所有相關的聲音,這樣就為聲音重播做好了準備。

6. 在 “Screens” 專案資料夾下打開 “GameplayScreen.cs” 檔並瀏覽到 UpdateLastCheackpoint 方法。我們將修改方法,這樣每透過一個檢查點,就會播放一首曲子:

(Code Snippet – 3D Game Development – Ex 2 Task 1 Step 6 – GameplayScreen PlaySound Checkpoint)

C#

private void UpdateLastCheackpoint()
{
    BoundingSphere marblePosition =
         marble.BoundingSphereTransformed;

    var tmp = lastCheackpointNode;
    while (tmp.Next != null)
    {
        // If the marble close to checkpoint save the checkpoint
        if (Math.Abs(Vector3.Distance(marblePosition.Center, 
            tmp.Next.Value)) <= marblePosition.Radius * 3)
        {
            AudioManager.PlaySound("checkpoint");
            lastCheackpointNode = tmp.Next;
            return;
        }
        tmp = tmp.Next;
    }

}

7. 在 “Objects” 專案資料夾下打開 “Marble.cs” 檔並瀏覽到 Update 方法。加入下面的程式碼以修改方法:

(Code Snippet – 3D Game Development – Ex 2 Task 1 Step 7 – Marble Call PlaySounds)

C#

public override void Update(GameTime gameTime)
{
    base.Update(gameTime);

    // Make the camera follow the marble
    Camera.ObjectToFollow = Vector3.Transform(Position, 
        Matrix.CreateFromYawPitchRoll(Maze.Rotation.Y,
        Maze.Rotation.X, Maze.Rotation.Z));

    PlaySounds();
}

“PlaySounds” 輔助方法將負責播放與彈球移動相關的聲音。我們將在下面的步驟中實現它。

8. 為 PlaySounds 方法添加實作:

(Code Snippet – 3D Game Development – Ex 2 Task 1 Step 8 – Marble PlaySounds)

C#

private void PlaySounds()
{
    // Calculate the pitch by the velocity
    float volumeX = MathHelper.Clamp(Math.Abs(Velocity.X) / 400,
                       0, 1);
    float volumeZ = MathHelper.Clamp(Math.Abs(Velocity.Z) / 400, 
                       0, 1);
    float volume = Math.Max(volumeX, volumeZ);
    float pitch = volume - 1.0f;

    // Play the roll sound only if the marble roll on maze
    if (intersectDetails.IntersectWithGround &&
        (Velocity.X != 0 || Velocity.Z != 0))
    {
        if (AudioManager.Instance["rolling"].State != 
            SoundState.Playing)
            AudioManager.PlaySound("rolling", true);

        // Update the volume & pitch by the velocity
        AudioManager.Instance["rolling"].Volume = 
            Math.Max(volumeX, volumeZ);
        AudioManager.Instance["rolling"].Pitch = pitch;
    }
    else
    {
        AudioManager.StopSound("rolling");
    }

    // Play fall sound when fall
    if (Position.Y < -50)
    {
        AudioManager.PlaySound("pit");
    }

    // Play collision sound when collide with walls
    if (intersectDetails.IntersectWithWalls)
    {
        AudioManager.PlaySound("collision");
        AudioManager.Instance["collision"].Volume = 
            Math.Max(volumeX, volumeZ);
    }
}

這個方法負責播放幾種聲音。當彈球滾動,就會播放滾動的聲音,並根據它當前的速度來調整聲音的音調和音量。當彈球碰到牆壁或掉進洞裡,這個方法還負責播放其他的聲音。

9. 編譯並部署專案。現在遊戲應該包括聲音了。


任務 2 – 更多的畫面和功能表

在以前的任務中,我們已經大大改進了遊戲的體驗性,但是遊戲開發還沒完成,因為當執行遊戲,畫面顯示的太突然並且一旦遊戲結束,就無法重播了 (缺少遊戲重啟功能)。此外,玩家不能暫停遊戲。

在本任務中,我們將添加更多的畫面和功能表,並把它們聯繫起來。

1. 在 “MarbleMazeGameContent” 專案中添加一個新的專案資料夾,並命名為 “Images”,然後把實驗安裝資料夾下的 Assets \ Media \ Images 裡的所有檔都添加到這個新資料夾裡。

2. 在 "Screens" 專案資料夾下添加一個名為 “BackgroundScreen” 的新類別。

3. 在新的類別檔開頭添加下列宣告:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 3 – BackgroundScreen Using Statements)

C#

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using GameStateManagement;

4. 修改新類別,使其成為 “GameScreen” 類別的衍生類別。

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 4 – BackgroundScreen Class Definition)

C#

class BackgroundScreen : GameScreen
{
}

**說明:**Do not forget to change the class’s namespace。

5. 添加下列類別變數,這些變數以後會用於載入背景圖片:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 5 – BackgroundScreen Background Field)

C#

Texture2D background;

6. 定義建構函式如下:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 6 – BackgroundScreen Constructor)

C#

public BackgroundScreen()
{
    TransitionOnTime = TimeSpan.FromSeconds(0.0);
    TransitionOffTime = TimeSpan.FromSeconds(0.5);
}

上述程式碼只是給來自於 GameScreen 類別的屬性設值,將用於控制畫面的進出轉換。

7. 覆寫父類別的 “LoadContent” 方法來載入背景圖像:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 7 – BackgroundScreen LoadContent)

C#

public override void LoadContent()
{
    background = Load<Texture2D>(@"Images\titleScreen");
}

8. 透過覆寫 Draw 方法來添加自訂的繪製邏輯:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 8 – BackgroundScreen Draw)

C#

public override void Draw(GameTime gameTime)
{
    SpriteBatch spriteBatch = ScreenManager.SpriteBatch;

    spriteBatch.Begin();

    spriteBatch.Draw(background, new Vector2(0, 0),
        Color.White * TransitionAlpha);

    spriteBatch.End();
}

9. 現在我們擁有了一個背景畫面,接下來在它上面添加一個功能表。在 “Screens” 專案資料夾裡建立一個名為 “MainMenuScreen” 的新類別。

10. 打開新的類別檔,在檔開頭添加下列宣告。

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 10 – MainMenuScreen Using Statements)

C#

using GameStateManagement;
using Microsoft.Xna.Framework;

11. 把新類別改成 “MenuScreen” 類別的衍生類別 (這個 screen 類別是在 ScreenManager 資料夾裡的程式碼中定義的):

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 11 – MainMenuScreen Class Definition)

C#

class MainMenuScreen : MenuScreen
{
}

說明:請記得要修改新類別的命名空間。

12. 在類別中添加如下建構函式。它定義了顯示功能表畫面上的功能表項目而且透過把 IsPopup 屬性設定為 true 使功能表顯示在背景畫面上:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 12 – MainMenuScreen Constructor)

C#

public MainMenuScreen()
    : base("")
{
    IsPopup = true;

    // Create our menu entries.
    MenuEntry startGameMenuEntry = new MenuEntry("Play");
    MenuEntry highScoreMenuEntry = new MenuEntry("High Score");
    MenuEntry exitMenuEntry = new MenuEntry("Exit");

    // Hook up menu event handlers.
    startGameMenuEntry.Selected += StartGameMenuEntrySelected;
    highScoreMenuEntry.Selected += HighScoreMenuEntrySelected;
    exitMenuEntry.Selected += OnCancel;

    // Add entries to the menu.
    MenuEntries.Add(startGameMenuEntry);
    MenuEntries.Add(highScoreMenuEntry);
    MenuEntries.Add(exitMenuEntry);
}

功能表畫面包含了描述功能表選項的 MenuEntry 物件。每個選項包含一個事件處理器,當玩家從功能表裡選中時,就會觸發事件處理器。你還能看到上述程式碼是如何為所有功能表選項設定處理器的。在下面的步驟中,我們添加指定的方法作為事件處理器。

13. 在類別中透過實現下列方法來建立事件處理器:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 13 – MainMenuScreen Event Handlers)

C#

void HighScoreMenuEntrySelected(object sender, EventArgs e)
{
    foreach (GameScreen screen in ScreenManager.GetScreens())
        screen.ExitScreen();

    ScreenManager.AddScreen(new BackgroundScreen(), null);
    ScreenManager.AddScreen(new HighScoreScreen(), null);
 }

void StartGameMenuEntrySelected(object sender, EventArgs e)
{
    foreach (GameScreen screen in ScreenManager.GetScreens())
        screen.ExitScreen();

    ScreenManager.AddScreen(new LoadingAndInstructionScreen(), null);
}

protected override void OnCancel(PlayerIndex playerIndex)
{
    HighScoreScreen.SaveHighscore();

    ScreenManager.Game.Exit();
}

注意前兩個方法和最後一個方法的不同之處。前兩個方法是實際的事件處理器,但是 OnCancel 是被另一個也叫 OnCancel 的事件觸發器調用的。後者是在父類別裡實現的。和畫面和功能表有關的各種處理器還沒實現。我們將在本任務中實現它們。

14. 在 “Screen” 專案資料夾下建立一個名為 “LoadingAndInstructionScreen” 的類別。

15. 打開新的類別檔,在檔開頭添加如下宣告。

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 15 – LoadingAndInstructionScreen Using Statements)

C#

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using GameStateManagement;
using Microsoft.Xna.Framework.Input.Touch;
using System.Threading;

16. 把新類別修改成 “GameScreen” 類別的衍生類別:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 16 – LoadingAndInstructionScreen Class Definition)

C#

class LoadingAndInstructionScreen : GameScreen
{

}

17. 在類別中添加下列欄位:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 17 – LoadingAndInstructionScreen Fields)

C#

Texture2D background;
SpriteFont font;
bool isLoading;
GameplayScreen gameplayScreen;
Thread thread;

你會注意到欄位裡有個 thread 物件,我們會馬上用到它。

18. 在類別中添加如下建構函式。因為畫面會對玩家在顯示器上的輕敲做出反應,我們需要啟用 tap gestures:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 18 – LoadingAndInstructionScreen Constructor)

C#

public LoadingAndInstructionScreen()
{
    EnabledGestures = GestureType.Tap;

    TransitionOnTime = TimeSpan.FromSeconds(0);
    TransitionOffTime = TimeSpan.FromSeconds(0.5);
}

19. 覆寫 “LoadContent” 方法來載入以後會用到的圖像指令集和字體:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 19 – LoadingAndInstructionScreen LoadContent)

C#

public override void LoadContent()
{
    background = Load<Texture2D>(@"Textures\instructions");
    font = Load<SpriteFont>(@"Fonts\MenuFont");

    // Create a new instance of the gameplay screen
    gameplayScreen = new GameplayScreen();
    gameplayScreen.ScreenManager = ScreenManager;
}

20. 按照如下的程式碼片段覆寫 HandleInput 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 20 – LoadingAndInstructionScreen HandleInput)

C#

public override void HandleInput(InputState input)
{
    if (!isLoading)
    {
        if (input.Gestures.Count > 0)
        {
            if (input.Gestures[0].GestureType == GestureType.Tap)
            {
                // Start loading the resources in additional thread
                thread = new Thread(
                    new ThreadStart(gameplayScreen.LoadAssets));

                isLoading = true;
                thread.Start();
            }
        }
    }
    base.HandleInput(input);
}

上述方法等待使用者的輕敲來顯示操作指南畫面。我們本想接著顯示遊戲畫面,但是等待它載入資源會在輕敲和畫面顯示之間產生明顯的延遲。因此,我們將建立一個新執行緒來執行遊戲畫面資源的初始化。我們將顯示一個載入提示直到載入過程結束,然後再顯示遊戲畫面。讓我們轉向等待資源載入的 Update 方法。

21. 用下面的程式碼覆寫 “Update” 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 21 – LoadingAndInstructionScreen Update)

C#

public override void Update(GameTime gameTime,
    bool otherScreenHasFocus, bool coveredByOtherScreen)
{
    // If additional thread is running, skip
    if (null != thread)
    {
        // If additional thread finished loading and the screen is
        // not exiting
        if (thread.ThreadState ==
            ThreadState.Stopped && !IsExiting)
        {
            // Exit the screen and show the gameplay screen 
            // with pre-loaded assets
            foreach (GameScreen screen in
                ScreenManager.GetScreens())
                screen.ExitScreen();

            ScreenManager.AddScreen(gameplayScreen, null);
        }
    }

    base.Update(gameTime, otherScreenHasFocus,
        coveredByOtherScreen);
}

22. 覆寫 Draw 方法,在遊戲資源載入時,顯示操作指南圖像和載入提示:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 22 – LoadingAndInstructionScreen Draw)

C#

public override void Draw(GameTime gameTime)
{
    SpriteBatch spriteBatch = ScreenManager.SpriteBatch;

    spriteBatch.Begin();

    // Draw Background
    spriteBatch.Draw(background, new Vector2(0, 0),
            new Color(255, 255, 255, TransitionAlpha));

    // If loading gameplay screen resource in the 
    // background show "Loading..." text
    if (isLoading)
    {
        string text = "Loading...";
        Vector2 size = font.MeasureString(text);
        Vector2 position = new Vector2(
            (ScreenManager.GraphicsDevice.Viewport.Width -
                size.X) / 2,
            (ScreenManager.GraphicsDevice.Viewport.Height -
                size.Y) / 2);
        spriteBatch.DrawString(font, text, position, Color.White);
    }

    spriteBatch.End();
}

23. 現在操作指南畫面載入了遊戲畫面的資源,所以我們不再需要在 GameplayScreen 類別裡執行這個操作了。打開 GameplayScreen.cs 類別檔,找到 “LoadContent” 方法,按照下面程式碼進行修改:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 23 – GameplayScreen Updated LoadContent)

C#

public override void LoadContent()
{
    timeFont = 
    ScreenManager.Game.Content.Load<SpriteFont>(@"Fonts\MenuFont");

    Accelerometer.Initialize();

    base.LoadContent();
}

24. 到目前為止,我們已經建立了 3 個額外的畫面,現在我們要使它們可以顯示。為了做到這一點,我們需要修改 “CatapultGame” 這個遊戲類別。打開 MarbleMazeGame.cs 檔,找到 MarbleMazeGame 類別的建構函式。在建構函式體內找到 “TODO” 注釋標籤,然後按照下面的程式碼直接替換,這樣附近的程式碼如下所示:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 24 – MarbleMazeGame Add Screens)

C#

public MarbleMazeGame()
{
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";

    // Frame rate is 30 fps by default for Windows Phone.
    TargetElapsedTime = TimeSpan.FromTicks(333333);
    //Create a new instance of the Screen Manager
    screenManager = new ScreenManager(this);
    Components.Add(screenManager);

    // Switch to full screen for best game experience
    graphics.IsFullScreen = true;

    graphics.SupportedOrientations =
        DisplayOrientation.LandscapeLeft;

    // Add two new screens
    screenManager.AddScreen(new BackgroundScreen(), null);
    screenManager.AddScreen(new MainMenuScreen(), null);

    // Initialize sound system
    AudioManager.Initialize(this);
}

注意我們去掉了添加遊戲畫面的行,並添加了背景和主功能表畫面的內容。

25. 我們需要是實現一個最後的畫面,功能表畫面會引用它。他就是高分畫面。在 "Screen" 專案資料夾下建立一個名為 "HighScoreScreen" 的類別。

26. 打開新的類別檔,並在檔開頭添加下列宣告。

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 26 – HighScoreScreen Using Statements)

C#

using System.Collections.Generic;
using System.Linq;
using System.IO.IsolatedStorage;
using System.IO;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using GameStateManagement;
using Microsoft.Xna.Framework.Input.Touch;

27. 修改新類別,將其改成 “GameScreen” 類別的衍生類別:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 27 – HighScoreScreen Class Definition)

C#

class HighScoreScreen : GameScreen
{

}

28. 在類別中添加下列欄位:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 28 – HighScoreScreen Fields)

C#

const int highscorePlaces = 10;
public static KeyValuePair<string, TimeSpan>[] highScore = new KeyValuePair<string, TimeSpan>[highscorePlaces] 
{
    new KeyValuePair<string,TimeSpan>
        ("Jasper",TimeSpan.FromSeconds(90)),
    new KeyValuePair<string,TimeSpan>
        ("Ellen",TimeSpan.FromSeconds(110)),
    new KeyValuePair<string,TimeSpan>
        ("Terry",TimeSpan.FromSeconds(130)),
    new KeyValuePair<string,TimeSpan>
        ("Lori",TimeSpan.FromSeconds(150)),
    new KeyValuePair<string,TimeSpan>
        ("Michael",TimeSpan.FromSeconds(170)),
    new KeyValuePair<string,TimeSpan>
        ("Carol",TimeSpan.FromSeconds(190)),
    new KeyValuePair<string,TimeSpan>
        ("Toni",TimeSpan.FromSeconds(210)),
    new KeyValuePair<string,TimeSpan>
        ("Cassie",TimeSpan.FromSeconds(230)),
    new KeyValuePair<string,TimeSpan>
        ("Luca",TimeSpan.FromSeconds(250)),
    new KeyValuePair<string,TimeSpan>
        ("Brian",TimeSpan.FromSeconds(270))
};

SpriteFont highScoreFont;

這些欄位定義了高分表的尺寸並提供一些預設項目。

29. 在 HighScoreScreen 類別中添加下列建構函式:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 29 – HighScoreScreen Constructor)

C#

public HighScoreScreen()
{
    EnabledGestures = GestureType.Tap;
}

30. 用下列程式碼覆寫父類別的 LoadContent 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 30 – HighScoreScreen LoadContent)

C#

public override void LoadContent()
{
    highScoreFont = Load<SpriteFont>(@"Fonts\MenuFont");

    base.LoadContent();
}

31. 用下面程式碼重新 HandleInput 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 31 – HighScoreScreen HandleInput)

C#

public override void HandleInput(InputState input)
{
    if (input == null)
        throw new ArgumentNullException("input");

    if (input.IsPauseGame(null))
    {
        Exit();
    }

    // Return to main menu when tap on the phone
    if (input.Gestures.Count > 0)
    {
        GestureSample sample = input.Gestures[0];
        if (sample.GestureType == GestureType.Tap)
        {
            Exit();

            input.Gestures.Clear();
        }
    }
}

當玩家輕敲顯示器或使用設備的 “back” 按鈕,它會使畫面退出。畫面的退出由即將實作的 “Exit” 方法管理。

32. 添加下列方法來退出高分畫面:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 32 – HighScoreScreen Exit)

C#

private void Exit()
{
    this.ExitScreen();
    ScreenManager.AddScreen(new BackgroundScreen(), null);
    ScreenManager.AddScreen(new MainMenuScreen(), null);
}

33. 覆寫 Draw 方法使高分表在畫面上顯示:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 33 – HighScoreScreen Draw)

C#

public override void Draw(Microsoft.Xna.Framework.GameTime gameTime)
{
    ScreenManager.SpriteBatch.Begin();

    // Draw the title
    ScreenManager.SpriteBatch.DrawString(highScoreFont, 
        "High Scores", new Vector2(30, 30), Color.White);

    // Draw the highscores table
    for (int i = 0; i < highScore.Length; i++)
    {
        ScreenManager.SpriteBatch.DrawString(highScoreFont, 
            String.Format("{0}. {1}", i + 1, highScore[i].Key),
            new Vector2(100, i * 40 + 70), Color.YellowGreen);
        ScreenManager.SpriteBatch.DrawString(highScoreFont, 
            String.Format("{0:00}:{1:00}",
            highScore[i].Value.Minutes, 
            highScore[i].Value.Seconds),
            new Vector2(500, i * 40 + 70), Color.YellowGreen);
    }

    ScreenManager.SpriteBatch.End();

    base.Draw(gameTime);
}

到目前為止,我們向畫面裡添加了很少管理高分表的邏輯。現在我們把重心轉移到這上面。

34. 向 HighScoreScreen 中添加下列方法。它將透過對一個分數和高分表裡最低分數進行比較的方法來判斷它是否屬於高分表:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 34 – HighScoreScreen IsInHighScores)

C#

public static bool IsInHighscores(TimeSpan gameTime)
{
    // If the score is less from the last place score
    return gameTime < highScore[highscorePlaces - 1].Value;
}

35. 添加一個新增的方法,用於對高分表裡的分數排序:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 35 – HighScoreScreen OrderGameScore)

C#

private static void OrderGameScore()
{
    highScore = (highScore.OrderBy(e => e.Value.Ticks)).ToArray();
}

高分表根據用時多少來排序的。

36. 添加一個方法來向高分表中插入新的分數:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 36 – HighScoreScreen PutHighScore)

C#

public static void PutHighScore(string playerName, TimeSpan gameTime)
{
    if (IsInHighscores(gameTime))
    {
        highScore[highscorePlaces - 1] = 
            new KeyValuePair<string, TimeSpan>(playerName,
                gameTime);
        OrderGameScore();
    }
}

它是透過去掉最低分來插入新的分數,然後對表進行排序。

37. 添加如下方法把高分表儲存到設備上:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 37 – HighScoreScreen SaveHighScore)

C#

public static void SaveHighscore()
{
    // Get the place to store the data
    using (IsolatedStorageFile isf = 
        IsolatedStorageFile.GetUserStoreForApplication())
    {
        // Create the file to save the data
        using (IsolatedStorageFileStream isfs = new
            IsolatedStorageFileStream("highscores.txt",
            FileMode.Create, isf))
        {
            // Get the stream to write the file
            using (StreamWriter writer = new StreamWriter(isfs))
            {
                for (int i = 0; i < highScore.Length; i++)
                {
                    // Write the scores
                    writer.WriteLine(highScore[i].Key);
                    writer.WriteLine(highScore[i].Value.ToString());
                }

                // Save and close the file
                writer.Flush();
                writer.Close();
            }
        }
    }
}

注意,這是我們第一次訪問遊戲的隔離儲存區 (Isolated Storage),它是遊戲在設備上保存資料的唯一地方。

38. 添加如下方法來載入高分表:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 38 – HighScoreScreen LoadHoghScore)

C#

public static void LoadHighscore()
{
    // Get the place the data stored
    using (IsolatedStorageFile isf =
        IsolatedStorageFile.GetUserStoreForApplication())
    {
        // Try to open the file
        if (isf.FileExists("highscores.txt"))
        {
            using (IsolatedStorageFileStream isfs = new
                IsolatedStorageFileStream("highscores.txt",
                FileMode.Open, isf))
            {
                // Get the stream to read the data
                using (StreamReader reader = new StreamReader(isfs))
                {
                    // Read the highscores
                    int i = 0;
                    while (!reader.EndOfStream)
                    {
                        string[] line = new[] { reader.ReadLine(),
                            reader.ReadLine() };
                        highScore[i++] = new KeyValuePair<string,
                            TimeSpan>(line[0],
                        TimeSpan.Parse(line[1]));
                    }
                }
            }
        }
    }

    OrderGameScore();
}

當載入高分表時,我們嘗試在遊戲的隔離儲存區因保存操作產生的檔案。如果檔案不存在,高分表將恢復它預設的資料。

39. 高分表已經就緒,我們只需要對其進行初始化。打開 “MableMazeGame.cs” 檔並瀏覽到 LoadContent 方法。按照下面的方式修改方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 39 – MarbleMazeGame Call LoadHighScore)

C#

protected override void LoadContent()
{
    AudioManager.LoadSounds();
    HighScoreScreen.LoadHighscore();
    base.LoadContent();
}

40. 編譯並部署專案,雖然遊戲本身結束時仍然顯得很突然,但是當遊戲執行時,你將會看到主功能表,


圖例 37
主功能表選單


圖例 38
高分表畫面

41. 任務的最後部分是添加一個畫面,即暫停畫面。這個畫面將允許玩家暫停遊戲並且外觀與主功能表畫面類別似。在 “Screen” 資料夾下建立一個名為 “PauseScreen” 的類別。

42. 打開新的類別檔。在檔開頭添加下列宣告。

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 42 – PauseScreen Using Statements)

C#

using System.Linq;
using Microsoft.Xna.Framework;
using GameStateManagement;

43. 修改新類別,使其繼承 “MenuScreen” 類別:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 43 – PauseScreen Class Definition)

C#

class PauseScreen : MenuScreen
{
}

44. 在類別中添加如下建構函式:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 44 – PauseScreen Constructor)

C#

public PauseScreen()
    : base("Game Paused")
{
    // Create our menu entries.
    MenuEntry returnGameMenuEntry = new MenuEntry("Return");
    MenuEntry restartGameMenuEntry = new MenuEntry("Restart");
    MenuEntry exitMenuEntry = new MenuEntry("Quit Game");

    // Hook up menu event handlers.
    returnGameMenuEntry.Selected += ReturnGameMenuEntrySelected;
    restartGameMenuEntry.Selected += RestartGameMenuEntrySelected;
    exitMenuEntry.Selected += OnCancel;

    // Add entries to the menu.
    MenuEntries.Add(returnGameMenuEntry);
    MenuEntries.Add(restartGameMenuEntry);
    MenuEntries.Add(exitMenuEntry);
}

暫停畫面顯示一個帶三個功能表項目的功能表。一個是允許玩家返回遊戲,另一個是允許玩家重新啟動遊戲,最後一個是允許玩家返回主功能表。

45. 在類別中添加如下時間處理器。它們會被暫停畫面中的功能表選項用到:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 45 – PauseScreen Event Handlers)

C#

void ReturnGameMenuEntrySelected(object sender, EventArgs e)
{
    AudioManager.PauseResumeSounds(true);

    var res = from screen in ScreenManager.GetScreens()
                where screen.GetType() != typeof(GameplayScreen)
                select screen;

    foreach (GameScreen screen in res)
        screen.ExitScreen();

    (ScreenManager.GetScreens()[0] as GameplayScreen).IsActive =
        true;
}

void RestartGameMenuEntrySelected(object sender, EventArgs e)
{
    AudioManager.PauseResumeSounds(true);

    var res = from screen in ScreenManager.GetScreens()
                where screen.GetType() != typeof(GameplayScreen)
                select screen;

    foreach (GameScreen screen in res)
        screen.ExitScreen();

    (ScreenManager.GetScreens()[0] as GameplayScreen).IsActive = true;

    (ScreenManager.GetScreens()[0] as GameplayScreen).Restart();
}

protected override void OnCancel(PlayerIndex playerIndex)
{
    foreach (GameScreen screen in ScreenManager.GetScreens())
        screen.ExitScreen();

    ScreenManager.AddScreen(new BackgroundScreen(), null);
    ScreenManager.AddScreen(new MainMenuScreen(), null);
}

當玩家希望返回遊戲時會觸發第一個處理器,注意這個處理器是如何恢復 IsActive 的值並重新開始所有暫停的聲音的。還要注意第二個處理器是如何使用一個 gameplay screen 裡尚未實現的方法的。

46. 從 “Screens” 專案資料夾中打開 “GameplayScreen.cs” 文件並瀏覽到 GameplayScreen 類別。添加如下方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 46 – GameplayScreen Restart)

C#

internal void Restart()
{
    marble.Position = maze.StartPoistion;
    marble.Velocity = Vector3.Zero;
    marble.Acceleration = Vector3.Zero;
    maze.Rotation = Vector3.Zero;
    IsActive = true;
    gameOver = false;
    gameTime = TimeSpan.Zero;
    lastCheackpointNode = maze.Checkpoints.First;
}

這個方法只是重設了一些會重置遊戲的變數。

47. 最後一步是修改 GameplayScreen 類別來初始化暫停畫面,添加如下方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 47 – GameplayScreen PauseCurrentGame)

C#

private void PauseCurrentGame()
{
    IsActive = false;
    // Pause the sounds
    AudioManager.PauseResumeSounds(false);

    ScreenManager.AddScreen(new BackgroundScreen(), null);
    ScreenManager.AddScreen(new PauseScreen(), null);
}

這個方法將暫停當前所有播放的聲音,使遊戲停止活動並轉到暫停畫面。

48. 修改 GameplayScreen 類別的建構函式:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 48 – GameplayScreen Updated Constructor)

C#

public GameplayScreen()
{
    TransitionOnTime = TimeSpan.FromSeconds(0.0);
    TransitionOffTime = TimeSpan.FromSeconds(0.0);

    EnabledGestures = GestureType.Tap;
}

49. 修改 GameplayScreen 類別的 HandleInput 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 49 – GameplayScreen Updated HandleInput)

C#

public override void HandleInput(InputState input)
{
   if (input == null)
      throw new ArgumentNullException("input");

   if (input.IsPauseGame(null))
   {
      if (!gameOver)
         PauseCurrentGame();
      else
         FinishCurrentGame();
   }

    if (IsActive)
    {
        if (input.Gestures.Count > 0)
        {
            GestureSample sample = input.Gestures[0];
            if (sample.GestureType == GestureType.Tap)
            {
                if (gameOver)
                    FinishCurrentGame();
            }
        }


        if (!gameOver)
        {
            // Rotate the maze according to accelerometer data
            Vector3 currentAccelerometerState =
                    Accelerometer.GetState().Acceleration;


            if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device)
            {
                //Change the velocity according to acceleration reading
                maze.Rotation.Z = (float)Math.Round(MathHelper.ToRadians(
                    currentAccelerometerState.Y * 30), 2);
                maze.Rotation.X = -(float)Math.Round(MathHelper.ToRadians(
                    currentAccelerometerState.X * 30), 2);
            }
            else if (Microsoft.Devices.Environment.DeviceType == 
                DeviceType.Emulator)
            {
                Vector3 Rotation = Vector3.Zero;

                if (currentAccelerometerState.X != 0)
                {
                    if (currentAccelerometerState.X > 0)
                        Rotation += new Vector3(0, 0, -angularVelocity);
                    else
                        Rotation += new Vector3(0, 0, angularVelocity);
                }

                if (currentAccelerometerState.Y != 0)
                {
                    if (currentAccelerometerState.Y > 0)
                        Rotation += new Vector3(-angularVelocity, 0, 0);
                    else
                        Rotation += new Vector3(angularVelocity, 0, 0);
                }

                // Limit the rotation of the maze to 30 degrees
                maze.Rotation.X =
                    MathHelper.Clamp(maze.Rotation.X + Rotation.X,
                    MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

                maze.Rotation.Z =
                    MathHelper.Clamp(maze.Rotation.Z + Rotation.Z,
                    MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

            }
        }
    }
}

50. 添加 FinishCurrentGame 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 2 Step 50 – GameplayScreen FinishCurrentGame)

C#

private void FinishCurrentGame()
{
    IsActive = false;

    foreach (GameScreen screen in ScreenManager.GetScreens())
        screen.ExitScreen();

    if (HighScoreScreen.IsInHighscores(gameTime))
    {
        // Show the device's keyboard
        Guide.BeginShowKeyboardInput(PlayerIndex.One,
        "Player Name", "Enter your name (max 15 characters)", "Player", 
        (r) =>
        {
            string playerName = Guide.EndShowKeyboardInput(r);

            if (playerName != null && playerName.Length > 15)
                playerName = playerName.Substring(0, 15);

            HighScoreScreen.PutHighScore(playerName, gameTime);

            ScreenManager.AddScreen(new BackgroundScreen(), null);
            ScreenManager.AddScreen(new HighScoreScreen(), null);

        }, null);
        return;
    }

    ScreenManager.AddScreen(new BackgroundScreen(), null);
    ScreenManager.AddScreen(new HighScoreScreen(), null);
}

當玩家達到了一個高分時,這個更新的方法允許玩家輸入自己的名字。

51. 編譯並部署專案。現在你應該可以在遊戲畫面時按下 “back” 按鈕來暫停遊戲了。此外,暫停畫面功能表中的選項都應該工作正常。


圖例 4
暫停畫面


任務 3 – “3 - 2 - 1 - Go!” 倒數計時器 (Countdown Timer) 和遊戲結束畫面

在這個任務中,我們將致力於當遊戲開始或結束時,更平滑的顯示遊戲畫面和退出遊戲。

1. 在 “Screens” 專案資料夾下,打開 “GameplayScreen.cs” 文件並向 GameplayScreen 類別中添加下列欄位:

(Code Snippet – 3D Game Development – Ex 2 Task 3 Step 1 – GameplayScreen Additional Fields)

C#

bool startScreen = true;
TimeSpan startScreenTime = TimeSpan.FromSeconds(4);

2. 修改 Update 方法使其包含下列程式碼:

(Code Snippet – 3D Game Development – Ex 2 Task 3 Step 2 – GameplayScreen Update)

C#

public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
    if (IsActive && !gameOver)
    {
        if (!startScreen)
        {
            // Calculate the time from the start of the game
            this.gameTime += gameTime.ElapsedGameTime;

            CheckFallInPit();
            UpdateLastCheackpoint();
        }

        // Update all the component of the game
        maze.Update(gameTime);
        marble.Update(gameTime);
        camera.Update(gameTime);

        CheckGameFinish();

        base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
    }
    if (startScreen)
    {
        if (startScreenTime.Ticks > 0)
        {
            startScreenTime -= gameTime.ElapsedGameTime;
        }
        else
        {
            startScreen = false;
        }
    }
}

上面的程式碼引入了一個在遊戲畫面首次出現和遊戲真正開始之間的延遲。它還負責在玩家到達終點之後和遊戲結束之前之間添加延遲。

3. 更新 Draw 方法如下:

(Code Snippet – 3D Game Development – Ex 2 Task 3 Step 3 – GameplayScreen Updated Draw)

C#

public override void Draw(GameTime gameTime)
{
    ScreenManager.GraphicsDevice.Clear(Color.Black);
    ScreenManager.SpriteBatch.Begin();
    if (startScreen)
    {
        DrawStartGame(gameTime);
    }
    if (IsActive)
    {
        // Draw the elapsed time
        ScreenManager.SpriteBatch.DrawString(timeFont,
            String.Format("{0:00}:{1:00}", this.gameTime.Minutes,
            this.gameTime.Seconds), new Vector2(20, 20),
            Color.YellowGreen);

        // Drawing sprites changes some render states around, which don't
        // play nicely with 3d models. 
        // In particular, we need to enable the depth buffer.
        DepthStencilState depthStensilState =
            new DepthStencilState() { DepthBufferEnable = true };
        ScreenManager.GraphicsDevice.DepthStencilState = depthStensilState;

        // Draw all the game components
        maze.Draw(gameTime);
        marble.Draw(gameTime);
    }
    if (gameOver)
    {
        AudioManager.StopSounds();
        DrawEndGame(gameTime);
    }

    ScreenManager.SpriteBatch.End();
    base.Draw(gameTime);
}

這裡我們叫用的是在遊戲開始之前和結束之後繪製螢幕提示的方法。

4. 在 GameplayScreen 類別中添加下列程式碼:

(Code Snippet – 3D Game Development – Ex 2 Task 3 Step 4 – GameplayScreen DrawEndGame)

C#

private void DrawEndGame(GameTime gameTime)
{
    string text = HighScoreScreen.IsInHighscores(this.gameTime) ? 
        "    You got a High Score!" : "          Game Over";
    text += "\nTouch the screen to continue";
    Vector2 size = timeFont.MeasureString(text);
    Vector2 textPosition = (new 
        Vector2(ScreenManager.GraphicsDevice.Viewport.Width, 
        ScreenManager.GraphicsDevice.Viewport.Height) - size) / 2f;

    ScreenManager.SpriteBatch.DrawString(timeFont, text,
        textPosition, Color.White);
}

private void DrawStartGame(GameTime gameTime)
{
    string text = (startScreenTime.Seconds == 0) ? "Go!" : 
        startScreenTime.Seconds.ToString();
    Vector2 size = timeFont.MeasureString(text);
    Vector2 textPosition = (new 
        Vector2(ScreenManager.GraphicsDevice.Viewport.Width, 
        ScreenManager.GraphicsDevice.Viewport.Height) - size) / 2f;

    ScreenManager.SpriteBatch.DrawString(timeFont, text, textPosition, 
    Color.White);
}

5. 透過修改初始化條件和 Tap 處理來修改 HandleInput 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 3 Step 5 – GameplayScreen Initial Condition)

C#

public override void HandleInput(InputState input)
{
   if (input == null)
      throw new ArgumentNullException("input");
 
   if (input.IsPauseGame(null))
   {
      if (!gameOver)
         PauseCurrentGame();
      else
         FinishCurrentGame();
   }

    if (IsActive && !startScreen)
    {
        
...

在遊戲初始化倒數計時期間,上述程式碼會使遊戲忽略用戶輸入。

6. 修改 GameplayScreen 類別的 Restart 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 3 Step 6 – GameplayScreen Set StartScreen)

C#

internal void Restart()
{
    marble.Position = maze.StartPoistion;
    marble.Velocity = Vector3.Zero;
    marble.Acceleration = Vector3.Zero;
    maze.Rotation = Vector3.Zero;
    IsActive = true;
    gameOver = false;
    gameTime = TimeSpan.Zero;
    startScreen = true;
    startScreenTime = TimeSpan.FromSeconds(4);
    lastCheackpointNode = maze.Checkpoints.First;
}

7. 編譯並部署專案。現在的遊戲是完全可玩的了。遊戲將會平順地開始結束,並且你可以保存你的高分。唯一剩下的任務是添加一個校準畫面。


任務 4 – 校準畫面

我們最後的任務是添加一個校準畫面,它將執行玩家校準加速器來消除 ”白色雜訊 (white noise)”。

1. 在 “Screens” 專案資料夾下建立一個名為 “CalibrationScreen” 的類別。

2. 打開新的類別檔並在檔開頭添加如下宣告。

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 1 – CalibrationScreen Using Statements)

C#

using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using GameStateManagement;
using System.Threading;

3. 修改新類別,使其繼承 “MenuScreen” 類別:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 3 – CalibrationScreen Class Definition)

C#

class CalibrationScreen : GameScreen
{
}

**說明:**Do not forget to alter the class’s namespace。

4. 在新類別中添加下列欄位:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 4 – CalibrationScreen Fields)

C#

Texture2D background;
SpriteFont font;
bool isCalibrating;
GameplayScreen gameplayScreen;
Thread thread;
 
// Calibration data
Microsoft.Devices.Sensors.Accelerometer accelerometer;
Vector3 accelerometerState = Vector3.Zero;
Vector3 accelerometerCalibrationData = Vector3.Zero;
DateTime startTime;
long samplesCount = 0;

5. 在類別中添加如下建構函式:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 5 – CalibrationScreen Constructor)

C#

C#
public CalibrationScreen(GameplayScreen gameplayScreen)
{
    TransitionOnTime = TimeSpan.FromSeconds(0);
    TransitionOffTime = TimeSpan.FromSeconds(0.5);

    IsPopup = true;
    this.gameplayScreen = gameplayScreen;
}

6. 覆寫 LoadContent 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 6 – CalibrationScreen LoadContent)

C#

public override void LoadContent()
{
    background = Load<Texture2D>(@"Images\titleScreen");
    font = Load<SpriteFont>(@"Fonts\MenuFont");

    // Start calibrating in additional thread
    thread = new Thread(
        new ThreadStart(Calibrate));
    isCalibrating = true;
    startTime = DateTime.Now;
    thread.Start();
}

7. 覆寫 Update 方法:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 7 – CalibrationScreen Update)

C#

public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
    // If additional thread is running, skip
    if (!isCalibrating)
    {
        gameplayScreen.AccelerometerCalibrationData = 
            accelerometerCalibrationData;
        foreach (GameScreen screen in ScreenManager.GetScreens())
            if (screen.GetType() == typeof(BackgroundScreen))
            {
                screen.ExitScreen();
                break;
            }

        (ScreenManager.GetScreens()[0] as GameplayScreen).IsActive = true;

        ExitScreen();
    }

    base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
}

上面所有的方法是等待校準過程結束,把資料存到 gameplay screen 並重新啟動它。

8. 覆寫 Draw 方法,這樣在校準的時候,會顯示提示:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 8 – CalibrationScreen Draw)

C#

public override void Draw(GameTime gameTime)
{
    SpriteBatch spriteBatch = ScreenManager.SpriteBatch;

    spriteBatch.Begin();

    // Draw Background
    spriteBatch.Draw(background, new Vector2(0, 0),
            new Color(255, 255, 255, TransitionAlpha));

    if (isCalibrating)
    {
        string text = "Calibrating...";
        Vector2 size = font.MeasureString(text);
        Vector2 position = new Vector2(
            (ScreenManager.GraphicsDevice.Viewport.Width - size.X) / 2,
            (ScreenManager.GraphicsDevice.Viewport.Height - size.Y) / 2);
        spriteBatch.DrawString(font, text, position, Color.White);
    }

    spriteBatch.End();
}

9. 添加下列方法來校準加速器:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 9 – CalibrationScreen Calibrate)

C#

private void Calibrate()
{
    //Initialize the accelerometer
    accelerometer = new Microsoft.Devices.Sensors.Accelerometer();

    if (accelerometer.State == SensorState.Initializing ||
        accelerometer.State == SensorState.Ready)
    {
        accelerometer.ReadingChanged += (s, e) =>
        {
            accelerometerState = new Vector3((float)e.X, (float)e.Y,
                (float)e.Z);

            samplesCount++;
            accelerometerCalibrationData += accelerometerState;

            if (DateTime.Now >= startTime.AddSeconds(5))
            {
                accelerometer.Stop();

                accelerometerCalibrationData.X /= samplesCount;
                accelerometerCalibrationData.Y /= samplesCount;
                accelerometerCalibrationData.Z /= samplesCount;

                isCalibrating = false;
            }
        };
    }
    accelerometer.Start();
}

在這個方法中,Calibration Screen 會積累每 5 秒鐘加速器的資料並計算出平均值。Gameplay screen 將使用這些值來調整遊戲過程中加速器讀取的數值。

10. 現在,我們需要做的是把校準畫面鉤在遊戲畫面上。瀏覽到 GameplayScreen 類別的建構函式,並進行如下修改:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 10 – GameplayScreen Enable DoubleTap)

C#

public GameplayScreen()
{
    TransitionOnTime = TimeSpan.FromSeconds(0.0);
    TransitionOffTime = TimeSpan.FromSeconds(0.0);

    EnabledGestures = GestureType.Tap | GestureType.DoubleTap;
}

11. 從 “Screens” 專案資料夾下打開 “GameplayScreen.cs” 檔並在 GameplayScreen 類別中添加下列方法:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 11 – GameplayScreen CalibrateGame)

C#

private void CalibrateGame()
{
    IsActive = false;
    // Pause the sounds
    AudioManager.PauseResumeSounds(false);
 
    ScreenManager.AddScreen(new BackgroundScreen(), null);
    ScreenManager.AddScreen(new CalibrationScreen(this), null);
}

上述方法只是啟動校準畫面。

12. 最後更新 HandleInput 方法,當螢幕被按兩下時,它會執行校準畫面:

(Code Snippet – 3D Game Development – Ex 2 Task 4 Step 12 – GameplayScreen Updated HandleInput)

C#

public override void HandleInput(InputState input)
{
   if (input == null)
      throw new ArgumentNullException("input");

   if (input.IsPauseGame(null))
   {
      if (!gameOver)
         PauseCurrentGame();
      else
         FinishCurrentGame();
   }

    if (IsActive && !startScreen)
    {
        if (input.Gestures.Count > 0)
        {
            GestureSample sample = input.Gestures[0];
            if (sample.GestureType == GestureType.Tap)
            {
                if (gameOver)
                    FinishCurrentGame();
            }
        }


        if (!gameOver)
        {
            if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device)
            {
                // Calibrate the accelerometer upon a double tap
                if (input.Gestures.Count > 0)
                {
                    GestureSample sample = input.Gestures[0];
                    if (sample.GestureType == GestureType.DoubleTap)
                    {
                        CalibrateGame();

                        input.Gestures.Clear();
                    }
                }
            }
            // Rotate the maze according to accelerometer data
            Vector3 currentAccelerometerState =
                Accelerometer.GetState().Acceleration;

            currentAccelerometerState.X -= AccelerometerCalibrationData.X;
            currentAccelerometerState.Y -= AccelerometerCalibrationData.Y;
            currentAccelerometerState.Z -= AccelerometerCalibrationData.Z;

            if (Microsoft.Devices.Environment.DeviceType == DeviceType.Device)
            {
                //Change the velocity according to acceleration reading
                maze.Rotation.Z = (float)Math.Round(MathHelper.ToRadians(
                    currentAccelerometerState.Y * 30), 2);
                maze.Rotation.X = -(float)Math.Round(MathHelper.ToRadians(
                    currentAccelerometerState.X * 30), 2);
            }
            else if (Microsoft.Devices.Environment.DeviceType == 
                DeviceType.Emulator)
            {
                Vector3 Rotation = Vector3.Zero;

                if (currentAccelerometerState.X != 0)
                {
                    if (currentAccelerometerState.X > 0)
                        Rotation += new Vector3(0, 0, -angularVelocity);
                    else
                        Rotation += new Vector3(0, 0, angularVelocity);
                }

                if (currentAccelerometerState.Y != 0)
                {
                    if (currentAccelerometerState.Y > 0)
                        Rotation += new Vector3(-angularVelocity, 0, 0);
                    else
                        Rotation += new Vector3(angularVelocity, 0, 0);
                }

                // Limit the rotation of the maze to 30 degrees
                maze.Rotation.X =
                    MathHelper.Clamp(maze.Rotation.X + Rotation.X,
                    MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

                maze.Rotation.Z =
                    MathHelper.Clamp(maze.Rotation.Z + Rotation.Z,
                    MathHelper.ToRadians(-30), MathHelper.ToRadians(30));

            }
        }
    }
}

13. 編譯並部署遊戲。遊戲中,當你按兩下螢幕,就可以瀏覽校準畫面了。

恭喜你!現在遊戲是完全可玩的了。


總結

本次實驗向你介紹了用 XNA 框架為 Windows Phone 7™ 開發 3D 遊戲應用。在本次實驗中,你為 Windows Phone 7 建立了一個 XNA Game Studio 專案,載入了遊戲資源,處理了輸入,更新了遊戲狀態並添加了遊戲的具體邏輯。

透過完成本次上機實驗,你也熟悉了為 Windows Phone 建立和測試一個 XNA Game Studio 專案所需的工具。在本次實驗中,你用 Visual Studio 2010 和 Windows Phone 開發工具為 Windows Phone 7 建立了一個新的 XNA Game Studio 應用,然後建立了應用程式的邏輯並設計了使用者介面。