定義主要遊戲物件

注意

本主題屬於<使用 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 呼叫一次。
  • OnSuspendingOnResuming。 分別是暫止/繼續遊戲的音訊。

以下是私用成員函式。

  • LoadSavedStateSaveState。 分別是載入/儲存遊戲的目前狀態。
  • LoadHighScoreSaveHighScore。 分別是載入/儲存所有遊戲的高分。
  • InitializeAmmo。 對於所有作為彈藥的球體物件,將其狀態重設為每回合開始的原始狀態。
  • UpdateDynamics。 這個方法很重要,因為其可根據罐頭動畫常式、物理和控制項輸入更新所有遊戲物件。 這是定義遊戲互動性的核心。 這點在<更新遊戲世界>一節中有相關說明。

其他公用方法是屬性存取子,可將遊戲和重疊的特定資訊傳回應用程式架構,以供顯示。

資料成員

這些物件會在遊戲迴圈執行時更新。

  • MoveLookController 物件。 代表玩家輸入。 如需詳細資訊,請參閱<新增控制項>。
  • GameRenderer 物件。 代表 Direct3D 11 轉譯器,可處理所有裝置特定物件及其轉譯。 如需詳細資訊,請參閱<轉譯架構 I>。
  • Audio 物件。 控制遊戲的音訊播放。 如需更多資訊,請參閱<新增音效>。

遊戲變數的其餘部分包含:基本類型清單及其各自的遊戲內數量,以及遊戲特定的資料和限制條件。

下一步

我們尚未討論真正的轉譯引擎原理:對已更新的基本類型呼叫 Render 方法如何轉換為螢幕上的像素。 這部分會在<轉譯架構 I:轉譯簡介>和<轉譯架構 II:遊戲轉譯>這兩個主題中進行說明。 如果您更想知道玩家控制項如何更新遊戲狀態,請參閱<新增控制項>。