備註
本主題是 使用 DirectX 教學課程系列建立簡單的通用 Windows 平臺 (UWP) 遊戲的一部分。 該連結中的主題設定了整個系列的背景。
在您配置範例遊戲的基本架構,並實作處理高階使用者和系統行為的狀態機器之後,您會想要檢查將範例遊戲變成遊戲的規則和機制。 讓我們看看範例遊戲主要對象的詳細數據,以及如何將遊戲規則轉譯成與遊戲世界的互動。
目標
- 瞭解如何套用基本的開發技術,以實作 UWP DirectX 遊戲的遊戲規則和機制。
主要遊戲物件
在 Simple3DGameDX 範例遊戲中,Simple3DGame 是主要遊戲物件類別。 透過 App::Load 方法,間接建構 Simple3DGame 實例。
以下是 Simple3DGame 類別的一些功能。
- 包含遊戲邏輯的實作。
- 包含傳達這些詳細數據的方法。
- 在應用程式架構中定義的狀態機器下,遊戲狀態進行變更。
- 遊戲狀態從應用程式變更為遊戲物件本身。
- 更新遊戲使用者介面(疊加層和抬頭顯示器)、動畫和物理模擬(動態)的詳細資訊。
備註
更新圖形是由 GameRenderer 類別處理,其中包含用於取得和使用遊戲圖形裝置資源的方法。 如需詳細資料,請參閱 渲染架構 I:渲染入門。
- 根據您在高層次上如何定義遊戲,作為定義遊戲會話、層級或生命周期的數據容器。 在此情況下,遊戲狀態數據是遊戲的存留期,而且會在用戶啟動遊戲時初始化一次。
若要檢視這個類別所定義的方法和數據,請參閱下方的 Simple3DGame 類別。
初始化並啟動遊戲
玩家啟動遊戲時,遊戲對象必須初始化其狀態、建立並新增重疊、設定追蹤玩家效能的變數,以及具現化將用來建置層級的物件。 在此範例中,當 App::Load中建立 GameMain 實例時,就會完成此動作。
GameMain::GameMain 建構函式中會建立類型為 Simple3DGame的遊戲物件。 接著,它會在 gameMain::ConstructInBackground fire-and-forget 協同程序期間,使用
Simple3DGame::Initialize 方法
範例遊戲會在遊戲對象中設定這些元件。
- 會建立新的音訊播放物件。
- 系統會建立遊戲圖形基本形狀的陣列,包括層級基本形狀、彈藥和障礙物的陣列。
- 系統會建立儲存遊戲狀態數據的位置,名為 Game,並將位置放在應用程式資料設定儲存位置,由 ApplicationData::Current所指定。
- 系統會建立遊戲定時器和初始遊戲內重疊位圖。
- 新的相機是使用一組特定的檢視和投影參數製作的。
- 輸入設備(控制器)設定為與相機相同的起始俯仰角和偏航角,因此玩家在起始控制位置和相機位置之間有一對一對應關係。
- 玩家物件已建立並設定為啟用。 我們使用球體對象來偵測玩家與牆壁和障礙物的鄰近性,並防止相機放置在可能會中斷沉浸的位置。
- 遊戲世界基本類型已建立。
- 已建立圓柱形障礙物。
- 系統會建立並編號目標 (Face 物件)。
- 彈藥球體已經被建立。
- 層級已經建立。
- 高分已載入。
- 載入任何先前儲存的遊戲狀態。
遊戲現在有所有關鍵元件的實例,即世界、玩家、障礙物、目標以及彈藥球體。 它也具有層級的實例,代表上述所有元件的組態,以及每個特定層級的行為。 現在讓我們看看遊戲如何建置關卡。
建置和載入遊戲關卡
在範例解決方案的 Level[N].h/.cpp 資料夾中的 檔案裡,已經完成了大部分的層級建構繁重任務。 因為它著重於非常特定的實作,所以我們不會在這裡涵蓋這些實作。 重要的是,每個層級的程式代碼都會以個別的 Level[N] 對象執行。 如果您想要擴充遊戲,您可以建立 Level[N] 物件,該物件會接受指派的數位作為參數,並隨機放置障礙和目標。 或者,您可以從資源檔,甚至是因特網載入層級組態數據。
定義遊戲玩法
此時,我們有開發遊戲所需的所有元件。 層級已從原始元素建構在記憶體中,並準備好讓玩家開始進行互動。
最好的遊戲會立即回應玩家輸入,並提供立即的意見反應。 對於任何類型的遊戲來說,從反應動作、即時第一人稱射擊遊戲,到深思熟慮的回合制策略遊戲,都是如此。
Simple3DGame::RunGame 方法
雖然遊戲層級正在進行中,但遊戲處於 Dynamics 狀態。
GameMain::Update 是一個主要的更新迴圈,每幀會更新一次應用程式狀態,如下所示。 更新迴圈會呼叫 Simple3DGame::RunGame 方法,以在遊戲處於 Dynamics 狀態時處理工作。
// 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 方法呼叫 Simple3DGame::UpdateDynamics 方法(此方法是從 GameMain::Update呼叫的),以更新遊戲場景中渲染的物件。
如 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 所定義的公用成員函式包含以下內容。
- 初始化。 設定全域變數的起始值,並初始化遊戲物件。 這在 初始化和啟動遊戲 一節中有所涵蓋。
- LoadGame。 初始化新的關卡,然後開始載入它。
- LoadLevelAsync。 初始化層級的協同程序,然後在繪圖引擎上調用另一個協同程序以載入針對裝置特定的層級資源。 這個方法會在個別的線程中執行;因此,只能從這個線程呼叫 ID3D11Device 方法(而不是 ID3D11DeviceContext 方法)。 FinalizeLoadLevel 方法中會呼叫任何裝置內容方法。 如果您不熟悉非同步程式設計,請參閱 C++/WinRT 的並行和非同步操作。
- FinalizeLoadLevel。 完成必須在主線程上執行的關卡載入的任何工作。 這包括對 Direct3D 11 裝置上下文(ID3D11DeviceContext)方法的任何呼叫。
- StartLevel。 啟動新關卡的遊戲。
- PauseGame。 暫停遊戲。
- RunGame。 執行遊戲迴圈的一次循環。 如果遊戲狀態是 進行中,每次遊戲迴圈將從 App::Update 呼叫一次。
- OnSuspending 和 OnResuming。 暫停或恢復遊戲的音效。
以下是私有成員函式。
- LoadSavedState 和 SaveState。 分別載入/儲存遊戲的目前狀態。
- LoadHighScore 和 SaveHighScore。 在多個遊戲中分別載入或儲存高分。
- InitializeAmmo。 將作為彈藥的每個球體物件的狀態重設為每個回合開頭的原始狀態。
- UpdateDynamics。 這是一個重要的方法,因為它會根據預設動畫程序、物理引擎和控制輸入來更新所有遊戲對象。 這是定義遊戲互動性的核心。 這涵蓋在 更新遊戲世界 一節中。
其他公用方法是屬性存取器,會將遊戲性和覆蓋層特定資訊傳遞至應用程式框架以供顯示。
數據成員
這些物件會在遊戲迴圈執行時更新。
- MoveLookController 物件。 代表玩家輸入。 如需詳細資訊,請參閱 新增控制件。
- GameRenderer 物件。 代表 Direct3D 11 渲染器,可處理所有特定於裝置的物件及其渲染。 如需詳細資訊,請參閱 渲染框架 I。
- 音頻 物件。 控制遊戲的音訊播放。 如需詳細資訊,請參閱 新增音效。
遊戲變數的其餘部分包含基元的清單與它們在遊戲中的數量,以及特定的遊戲數據和限制條件。
後續步驟
我們尚未討論實際的渲染引擎——如何在更新後的基元上呼叫 Render 方法,並呈現在螢幕上為像素。 這些層面涵蓋兩個部分:轉譯架構 I:轉譯 簡介和 轉譯架構II:遊戲轉譯。 如果您更有興趣玩家控制項如何更新遊戲狀態,請參閱 新增控制項。