定義主要遊戲物件
注意
本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。
配置範例遊戲的基本架構,並實作狀態機器以處理高階使用者及系統行為之後,接下來可進一步瞭解將範例遊戲轉為遊戲的規則和機制。 我們會詳細介紹範例遊戲的主要物件,以及如何將遊戲規則轉譯為與遊戲世界的互動。
目標
- 瞭解如何應用基本的開發技術,以實作 UWP DirectX 遊戲的遊戲規則和機制。
主要遊戲物件
在 Simple3DGameDX 範例遊戲中,Simple3DGame 是主要遊戲物件類別。 Simple3DGame 的執行個體是透過 App::Load 方法間接建構。
以下是 Simple3DGame 類別的一些功能。
- 包含遊戲邏輯的實作。
- 包含傳達這些詳細資料的方法。
- 遊戲狀態變更為應用程式架構中定義的狀態機器。
- 遊戲狀態從應用程式變更為遊戲物件本身。
- 更新遊戲 UI 的詳細資料 (重疊和平視顯示)、動畫和物理 (動態)。
注意
圖形更新作業是由 GameRenderer 類別處理,內含取得和使用圖形裝置資源 (用於遊戲) 的方法。 如需詳細資訊,請參閱<轉譯架構 I:轉譯簡介>。
- 視在較高層級定義遊戲的方式,可當作用來定義遊戲工作階段、關卡或存留期的資料容器。 在此情況下,遊戲狀態資料適用於遊戲的存留期,且於使用者啟動遊戲時初始化一次。
若要檢視此類別定義的方法和資料,請參閱<Simple3DGame 類別>。
初始化並啟動遊戲
玩家啟動遊戲時,遊戲物件必須初始化其狀態、建立並新增重疊、設定變數以追蹤玩家成績,以及具現化將用來建置層級的物件。 在此範例中,這是在 GameMain 執行個體 (建立於 App::Load) 中執行。
Simple3DGame 類型的遊戲物件是在 GameMain::GameMain 建構函式中建立。 接著,系統會在 GameMain::ConstructInBackground (從 GameMain::GameMain 呼叫) 自主導引協同程式期間,使用 Simple3DGame::Initialize 方法初始化遊戲物件。
Simple3DGame::Initialize 方法
範例遊戲會在遊戲物件中設定這些元件。
- 建立新的音訊播放物件。
- 建立遊戲圖形基本類型的陣列,包括關卡基本類型、彈藥和障礙物陣列。
- 建立用來儲存遊戲狀態的位置 (名稱為 Game,並放置於 ApplicationData::Current 指定的應用程式資料設定儲存位置。
- 建立遊戲計時器和初始遊戲內的重疊點陣圖。
- 使用一組特定的檢視和投影參數建立新相機。
- 將輸入裝置 (控制器) 設為與相機初始俯仰和偏航位置一致,使玩家的控制器位置和相機位置之間存在 1 對 1 的對應關係。
- 建立玩家物件並設為使用中。 使用球體物件偵測玩家與牆壁和障礙物的鄰近性,並防止相機置於可能中斷沉浸體驗的位置。
- 建立遊戲世界基本類型。
- 建立圓柱障礙物。
- 建立目標 (Face 物件) 並編號。
- 建立彈藥球體。
- 建立關卡。
- 載入高分。
- 載入任何先前儲存的遊戲狀態。
遊戲現已有所有重要元件 (世界、玩家、障礙物、目標及彈藥球體) 的執行個體。 也具有關卡執行個體,代表上述所有元件的組態,以及元件在各特定關卡的行為。 現在我們來看看遊戲如何建置關卡。
建置和載入遊戲關卡
建構關卡大部分的繁重工作都是在 Level[N].h/.cpp
檔案中完成 (位於範例解決方案的 GameLevels 資料夾)。 建構關卡著重於非常具體的實作,本文不涵蓋這些內容。 重點是各關卡的程式碼都以獨立的 Level[N] 物件執行。 若想延伸遊戲,可建立 Level[N] 物件,以將指派的數目作為參數,並隨機放置障礙物和目標。 或者,可讓物件從資源檔 (或甚至是網際網路) 載入關卡組態資料。
定義遊戲
目前我們已有開發遊戲所需的所有元件。 已從基本類型建構關卡並存於記憶體,準備好讓玩家開始互動。
出色的遊戲能立即回應玩家輸入,並提供立即的回饋。 無論是快速移動的即時第一人稱射擊遊戲,還是思考型的回合式策略遊戲,任何類型的遊戲都適用上述原則。
Simple3DGame::RunGame 方法
遊戲關卡進行中,但遊戲處於 Dynamics 狀態。
GameMain::Update 是主要更新迴圈 (每畫面格更新應用程式狀態一次),如下所示。 如果遊戲處於 Dynamics 狀態,更新迴圈會呼叫 Simple3DGame::RunGame 方法處理此工作。
// Updates the application state once per frame.
void GameMain::Update()
{
// The controller object has its own update loop.
m_controller->Update();
switch (m_updateState)
{
...
case UpdateEngineState::Dynamics:
if (m_controller->IsPauseRequested())
{
...
}
else
{
// When the player is playing, work is done by Simple3DGame::RunGame.
GameState runState = m_game->RunGame();
switch (runState)
{
...
Simple3DGame::RunGame 處理的資料會定義遊戲迴圈目前反覆運算的遊戲目前狀態。
以下是 Simple3DGame::RunGame 中的遊戲流程邏輯。
- 方法會更新計時器 (倒數計時秒數,直到關卡完成),並測試以檢查關卡時間是否到期。 這是遊戲規則之一:時間用完時,如未擊中所有目標,即遊戲結束。
- 如果時間已用完,則方法會設定 TimeExpired 遊戲狀態,並傳回至前述程式碼中的 Update 方法。
- 如果還有時間,則輪詢移動視角控制器,以更新相機位置;具體來說,是從相機平面更新檢視法線投影的角度 (玩家看向的位置),以及自上次輪詢控制器後該角度移動的距離。
- 根據移動視角控制器的新資料更新相機。
- 更新動態,即遊戲世界中與玩家控制項無關的物件動畫和行為。 在此範例遊戲中,呼叫 Simple3DGame::UpdateDynamics 方法,以更新觸發的彈藥球體動作、柱形障礙物的動畫及目標動作。 如需詳細資訊,請參閱<更新遊戲世界>。
- 方法會檢查是否已達到成功完成關卡的準則條件。 如果是,則確定關卡分數,並檢查是否為最後一關 (6)。 如果是最後一關,方法會 GameState::GameComplete 遊戲狀態;否則,即傳回 GameState::LevelComplete 遊戲狀態。
- 如果關卡未完成,方法會將遊戲狀態設為 GameState::Active 並傳回。
更新遊戲世界
在此範例中,遊戲執行時會從 Simple3DGame::RunGame 方法 (呼叫自 GameMain::Update) 呼叫 Simple3DGame::UpdateDynamics 方法,以更新遊戲場景中轉譯的物件。
像 UpdateDynamics 這類迴圈可呼叫方法,設定與玩家輸入無關的遊戲世界動態,以建立沉浸式遊戲體驗並使關卡栩栩如生。 這包括需要轉譯的圖形,以及執行動畫迴圈,以便在無玩家輸入時也能呈現動態世界。 就遊戲來說,這可能包括樹木在風中搖曳、岸邊湧起波浪、機械不斷冒煙,以及變形的外星怪物四處動來動去。 這也包括物件之間的互動,例如:玩家球體與世界碰撞,或彈藥與障礙物和目標碰撞。
除非遊戲已明確暫停,否則遊戲迴圈應持續更新遊戲世界,無論是基於遊戲邏輯、實體演算法進行更新,還是單純隨機式更新。
在範例遊戲中,此原則稱為「動態」,其包含柱狀障礙物的升降,以及彈藥球體觸發後在動態中的動作和實體行為。
Simple3DGame::UpdateDynamics 方法
此方法會處理這四組計算。
- 已觸發的彈藥球體在世界中的位置。
- 柱狀障礙物的動畫。
- 玩家和世界界限的交集。
- 彈藥球體與障礙物、目標、其他彈藥球體和世界的碰撞。
障礙物動畫發生於 Animate.h/.cpp 原始程式碼檔案定義的迴圈中。 彈藥及任何碰撞的行為是由簡化的物理演算法所定義,以程式碼提供並由遊戲世界的一組全域常數 (包括重力和材質屬性) 參數化。 全都是在遊戲世界的座標空間中進行計算。
檢閱流程
我們已更新場景中的所有物件並計算任何衝突,接著需要使用此資訊,繪製對應的視覺效果變更。
GameMain::Update 完成遊戲迴圈目前的反覆項目之後,範例會立即呼叫 GameRenderer::Render 以取得更新的物件資料,並產生新場景以呈現給玩家,如下所示。
void GameMain::Run()
{
while (!m_windowClosed)
{
if (m_visible)
{
switch (m_updateState)
{
case UpdateEngineState::Deactivated:
case UpdateEngineState::TooSmall:
...
// Otherwise, fall through and do normal processing to perform rendering.
default:
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
CoreProcessEventsOption::ProcessAllIfPresent);
// GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
// state, uses Simple3DGame::UpdateDynamics to update game world.
Update();
// Render is called immediately after the Update loop.
m_renderer->Render();
m_deviceResources->Present();
m_renderNeeded = false;
}
}
else
{
CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
CoreProcessEventsOption::ProcessOneAndAllPending);
}
}
m_game->OnSuspending(); // Exiting due to window close, so save state.
}
轉譯遊戲世界的圖形
建議經常更新遊戲中的圖形,最好與主要遊戲迴圈逐一查看一樣頻繁。 迴圈逐一查看時,無論是否有玩家輸入,遊戲世界的狀態都會更新。 這可讓計算出的動畫和行為順暢地顯示。 想像一個簡單的水域場景,只有當玩家按下按鈕時場景才會移動。 這樣就不符合現實;出色的遊戲看起來隨時都要自然流暢。
呼叫範本遊戲的迴圈,如上文 GameMain::Run 中所示。 如果遊戲的主視窗可見,且並未貼齊或停用,則遊戲會持續更新並轉譯該更新的結果。 我們接著檢視的 GameRenderer::Render 方法會轉譯該狀態的表示法。 這會在呼叫 GameMain::Update 後立即執行,其中包含 Simple3DGame::RunGame 以更新狀態,如上一節所述。
GameRenderer::Render 會繪製 3D 世界的投影,然後繪製 Direct2D 重疊其上。 完成時會呈現最後一個交換鏈結,其中包含要顯示的合併緩衝區。
注意
範例遊戲的 Direct2D 重疊有兩種狀態:一種是遊戲顯示遊戲資訊重疊,其中包含暫停功能表的點陣圖;另一種是遊戲顯示十字準線與矩形 (適用觸控螢幕移動視角控制器)。 分數文字會以兩種狀態繪製。 如需詳細資訊,請參閱<轉譯架構 I:轉譯簡介>。
GameRenderer::Render 方法
void GameRenderer::Render()
{
bool stereoEnabled{ m_deviceResources->GetStereoState() };
auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };
...
if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
{
// This section is only used after the game state has been initialized and all device
// resources needed for the game have been created and associated with the game objects.
...
for (auto&& object : m_game->RenderObjects())
{
object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
}
}
d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
d2dContext->BeginDraw();
// To handle the swapchain being pre-rotated, set the D2D transformation to include it.
d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());
if (m_game != nullptr && m_gameResourcesLoaded)
{
// This is only used after the game state has been initialized.
m_gameHud.Render(m_game);
}
if (m_gameInfoOverlay.Visible())
{
d2dContext->DrawBitmap(
m_gameInfoOverlay.Bitmap(),
m_gameInfoOverlayRect
);
}
...
}
}
Simple3DGame 類別
這些是 Simple3DGame 類別定義的方法和資料成員。
成員函式
Simple3DGame 定義的公用成員函式包含下列項目。
- Initialize。 設定全域變數的起始值,並初始化遊戲物件。 這點在<初始化並啟動遊戲>一節中有相關說明。
- LoadGame。 初始化新關卡並開始載入。
- LoadLevelAsync。 初始化關卡的協同程式,然後在轉譯器上叫用另一個協同程式,以載入裝置特定的關卡資源。 此方法會在個別的執行緒中執行;因此,由此執行緒僅可呼叫 ID3D11Device 方法 (不能使用 ID3D11DeviceContext 方法)。 在 FinalizeLoadLevel 方法中可呼叫任何裝置內容方法。 如果您不熟悉非同步程式設計,請參閱<透過 C++/WinRT 的並行和非同步作業>。
- FinalizeLoadLevel。 完成任何須在主執行緒完成的關卡載入工作。 這包括對 Direct3D 11 裝置內容 (ID3D11DeviceContext) 方法的任何呼叫。
- StartLevel。 啟動新關卡的遊戲。
- PauseGame。 暫停遊戲。
- RunGame。 執行遊戲迴圈的反覆項目。 如果遊戲狀態為 Active,則遊戲迴圈每次的反覆項目都會從 App::Update 呼叫一次。
- OnSuspending 和 OnResuming。 分別是暫止/繼續遊戲的音訊。
以下是私用成員函式。
- LoadSavedState 和 SaveState。 分別是載入/儲存遊戲的目前狀態。
- LoadHighScore 和 SaveHighScore。 分別是載入/儲存所有遊戲的高分。
- InitializeAmmo。 對於所有作為彈藥的球體物件,將其狀態重設為每回合開始的原始狀態。
- UpdateDynamics。 這個方法很重要,因為其可根據罐頭動畫常式、物理和控制項輸入更新所有遊戲物件。 這是定義遊戲互動性的核心。 這點在<更新遊戲世界>一節中有相關說明。
其他公用方法是屬性存取子,可將遊戲和重疊的特定資訊傳回應用程式架構,以供顯示。
資料成員
這些物件會在遊戲迴圈執行時更新。
- MoveLookController 物件。 代表玩家輸入。 如需詳細資訊,請參閱<新增控制項>。
- GameRenderer 物件。 代表 Direct3D 11 轉譯器,可處理所有裝置特定物件及其轉譯。 如需詳細資訊,請參閱<轉譯架構 I>。
- Audio 物件。 控制遊戲的音訊播放。 如需更多資訊,請參閱<新增音效>。
遊戲變數的其餘部分包含:基本類型清單及其各自的遊戲內數量,以及遊戲特定的資料和限制條件。
下一步
我們尚未討論真正的轉譯引擎原理:對已更新的基本類型呼叫 Render 方法如何轉換為螢幕上的像素。 這部分會在<轉譯架構 I:轉譯簡介>和<轉譯架構 II:遊戲轉譯>這兩個主題中進行說明。 如果您更想知道玩家控制項如何更新遊戲狀態,請參閱<新增控制項>。