Share via


Marble Maze 應用程式結構

DirectX 通用 Windows 平台 (UWP) 應用程式的結構與傳統傳統型應用程式不同。 Windows 執行階段不使用 HWND 等處理類型和 CreateWindow 等函式,而是提供 Windows::UI::Core::ICoreWindow 等介面,以便您可以以更現代、面向物件的方式開發 UWP 應用程式。 本檔的這一節說明 Marble Maze 應用程式程式碼的結構。

注意

與本文件對應的範例程式碼可以在 DirectX Marble Maze 遊戲範例中找到。

以下是本文件在建立遊戲程式碼時討論的一些要點:

  • 在初始化階段,設定遊戲使用的執行時間和程式庫元件,並載入遊戲特定的資源。
  • UWP 應用程式必須在啟動後 5 秒內開始處理事件。 因此,在載入應用程式時僅載入必要的資源。 遊戲應該在背景載入大量資源並顯示進度畫面。
  • 在遊戲迴圈中,回應 Windows 事件、讀取使用者輸入、更新場景物件,以及轉譯場景。
  • 使用事件處理常式來回應視窗事件。 (這些取代來自傳統型 Windows 應用程式的視窗訊息。)
  • 使用狀態機器來控制遊戲邏輯的流程和順序。

檔案組織

Marble Maze 中的一些元件可以在任何遊戲中重複使用,只需進行很少的修改或無需修改。 對於您自己的遊戲,您可以調整這些檔案提供的組織和想法。 以下資料表簡要描述了重要的原始碼檔案。

檔案 描述
App.h、App.cpp 定義 AppDirectXApplicationSource 類別,它們封裝應用程式的視圖 (視窗、執行緒和事件)。
Audio.h、Audio.cpp 定義 Audio 類別,用於管理音訊資源。
BasicLoader.h、BasicLoader.cpp 定義 BasicLoader 類別,該類別提供幫助您載入紋理、網格和著色器的公用程式方法。
BasicMath.h 定義結構和函式,協助您處理向量和矩陣資料和計算。 其中許多函式都與 HLSL 著色器類型相容。
BasicReaderWriter.h、BasicReaderWriter.cpp 定義 BasicReaderWriter 類別,該類別使用 Windows 執行階段在 UWP 應用程式中讀取和寫入檔案資料。
BasicShapes.h、BasicShapes.cpp 定義 BasicShapes 類別,該類別提供用於建立基本形狀 (例如立方體和球體) 的公用程式方法。 (Marble Maze 實作不會使用這些檔案)。
Camera.h、Camera.cpp 定義 Camera 類別,該類別提供相機的位置和方向。
Collision.h、Collision.cpp 管理彈珠和其他物件 (例如迷宮) 之間的衝突資訊。
DDSTextureLoader.h、DDSTextureLoader.cpp 定義 CreateDDSTextureFromMemory 函式,該函式從記憶體緩衝區載入 .dds 格式的紋理。
DirectXHelper.h 定義對許多 DirectX UWP 應用程式實用的 DirectX 協助程式函式。
LoadScreen.h、LoadScreen.cpp 定義 LoadScreen 類別,該類別在應用程式初始化期間顯示載入畫面。
MarbleMazeMain.h、MarbleMazeMain.cpp 定義 MarbleMazeMain 類別,該類別管理遊戲特定的資源並定義大部分遊戲邏輯。
MediaStreamer.h、MediaStreamer.cpp 定義 MediaStreamer 類別,該類別使用 Media Foundation 來幫助遊戲管理音訊資源。
PersistentState.h、PersistentState.cpp 定義 PersistentState 類別,該類別從備份存放區讀取原始資料類型,並向備份存放區寫入原始資料類型。
Physics.h、Physics.cpp 定義 Physics 類別,該類別實作彈珠和迷宮之間的實體模擬。
Primitives.h 定義遊戲所使用的幾何類型。
SampleOverlay.h、SampleOverlay.cpp 定義 SampleOverlay 類別,該類別提供常見的 2D 和使用者介面資料和操作。
SDKMesh.h、SDKMesh.cpp 定義 SDKMesh 類別,該類別載入並轉譯 SDK Mesh (.sdkmesh) 格式的網格。
StepTimer.h 定義 StepTimer 類別,該類別提供了一種取得總時間和經過時間的簡單方法。
UserInterface.h、UserInterface.cpp 定義與使用者介面相關的功能,例如功能表系統和高分資料表。

 

設計階段與執行階段資源格式

當您可以使用執行階段格式,而不是設計階段格式,以更有效率地載入遊戲資源。

設計階段格式是您設計資源時所使用的格式。 一般而言,3D 設計工具會使用設計階段格式。 某些設計階段格式也是以文字為基礎的格式,因此您可以在任何以文字為基礎的編輯器中修改它們。 設計階段格式可能很冗長,並且包含的資訊多於遊戲所需的資訊。 執行階段格式是遊戲所讀取的二進位格式。 執行階段格式通常比對應的設計階段格式更精簡且更有效率。 這就是為什麼大部分遊戲在執行階段使用執行間段資產的原因。

雖然您的遊戲可以直接讀取設計階段格式,但使用個別執行階段格式有幾個優點。 由於執行階段格式通常更精簡,因此它們需要更少的磁碟空間,並且透過網路傳輸所需的時間也更少。 此外,執行階段格式通常以記憶體對應資料結構表示。 因此,它們可以載入記憶體的速度比 XML 型文字檔快得多。 最後,由於個別的執行階段格式通常是二進位編碼,因此使用者較難修改。

HLSL 著色器是使用不同設計階段和執行階段格式的資源範例之一。 Marble Maze 使用 .hlsl 做為設計階段格式,而 .cso 則使用 .cso 做為執行階段格式。 .hlsl 檔案會保存著色器的原始程式碼;.cso 檔案會保存對應的著色器位元組程式碼。 當您離線轉換 .hlsl 檔案並隨遊戲提供 .cso 檔案時,您就無需在遊戲載入時將 HLSL 原始檔轉換為位元組代碼。

出於教學原因,Marble Maze 專案包括許多資源的設計階段格式和執行階段格式,但您只需在自己的遊戲的來源專案中維護設計階段格式,因為您可以在需要時將它們轉換為執行階段格式。 本文件說明如何將設計階段格式轉換成執行階段格式。

應用程式生命週期

Marble Maze 遵循一般 UWP 應用程式的生命週期。 如需 UWP app 生命週期的詳細資訊,請參閱應用程式生命週期

UWP 遊戲初始化時,通常會初始化執行階段元件,例如 Direct3D、Direct2D,以及它使用的任何輸入、音訊或物理程式庫。 它也會載入遊戲開始之前所需的遊戲特定資源。 此初始化會在遊戲工作階段期間發生一次。

初始化之後,遊戲通常會執行遊戲迴圈。 在此迴圈中,遊戲通常會執行四個動作:處理 Windows 事件、收集輸入、更新場景物件,以及轉譯場景。 當遊戲更新場景時,它可以將目前的輸入狀態套用至場景物件,並模擬實體事件,例如物件衝突。 遊戲也可以執行其他活動,例如播放音效或透過網路傳送資料。 當遊戲轉譯場景時,它會擷取場景的目前狀態,並將其繪製到顯示裝置。 下列各節更詳細地描述了這些活動。

新增至範本

DirectX 11 應用程式 (通用 Windows) 範本會建立一個可以使用 Direct3D 轉譯的核心視窗。 此範本也包括 DeviceResources 類別,該類別會建立在 UWP 應用程式中,轉譯 3D 內容所需的所有 Direct3D 裝置資源。

App 類別會建立 MarbleMazeMain 類別物件,開始載入資源,迴圈更新計時器,並且每個畫面都呼叫 MarbleMazeMain::Render 方法。 App::OnWindowSizeChangedApp::OnDpiChangedApp::OnOrientationChanged 方法會各自呼叫 MarbleMazeMain::CreateWindowSizeDependentResources 方法,App::Run 方法會呼叫 MarbleMazeMain::UpdateMarbleMazeMain::Render 方法。

以下範例顯示 App::SetWindow 方法建立 MarbleMazeMain 類別物件的位置。 DeviceResources 類別被傳遞給該方法,以便它可以使用 Direct3D 物件進行轉譯。

    m_main = std::unique_ptr<MarbleMazeMain>(new MarbleMazeMain(m_deviceResources));

App 類別也會開始載入遊戲的延遲資源。 請參閱下一節,了解詳細資訊。

此外,App 類別也為 CoreWindow 事件設定事件處理常式。 當呼叫這些事件的處理常式時,它們會將輸入傳遞給 MarbleMazeMain 類別。

在背景載入遊戲資產

為確保您的遊戲啟動後 5 秒內能夠回應視窗事件,我們建議您非同步載入遊戲資源,或在背景載入。 當資源在背景載入時,您的遊戲可以回應視窗事件。

注意

您也可以在主功能表準備就緒時顯示主功能表,並允許其餘資產繼續在背景載入。 例如,如果使用者在載入所有資源之前從功能表中選取一個選項,您可以透過顯示進度列來指出場景資源正在繼續載入。

 

即使您的遊戲包含相對較少的遊戲資源,非同步載入它們也是一個很好的做法,原因有兩個。 原因之一是很難保證所有資源都能在所有裝置和所有設定上快速載入。 此外,透過儘早合併非同步載入,您的程式碼可以在新增功能時進行擴展。

非同步資源載入從 App::Load 方法開始。 此方法使用工作類別在背景載入遊戲資源。

    task<void>([=]()
    {
        m_main->LoadDeferredResources(true, false);
    });

MarbleMazeMain 類別會定義 m_deferredResourcesReady 旗標來指出非同步載入已完成。 MarbleMazeMain::LoadDeferredResources 方法會載入遊戲資源,然後設定此旗標。 應用程式的更新 (MarbleMazeMain::Update) 和轉譯 (MarbleMazeMain::Render) 階段會檢查此旗標。 設定此旗標時,遊戲會如常繼續。 如果尚未設定旗標,遊戲會顯示載入畫面。

有關 UWP 應用非同步程式設計的詳細資訊,請參閱 C++ 中的非同步程式設計

提示

如果您編寫的遊戲程式碼是 Windows 執行時間 C++ 程式庫 (即 DLL) 的一部分,請考慮是否閱讀為 UWP 應用程式使用 C++ 建立非同步操作,以瞭解如何建立可由應用程式使用的非同步操作和其他程式庫。

 

遊戲迴圈

App::Run 方法會執行主遊戲迴圈 (MarbleMazeMain::Update)。 此方法稱為每個畫面。

為了幫助將檢視和視窗程式碼與遊戲特定的程式碼區分開,我們實作了 App::Run 方法以將更新和轉譯呼叫轉送到 MarbleMazeMain 物件。

以下範例顯示了 App::Run 方法,其中包括主遊戲迴圈。 遊戲迴圈會更新總時間和畫面時間變數,然後更新並轉譯場景。 這也可確保只有在視窗可見時才會呈現內容。

void App::Run()
{
    while (!m_windowClosed)
    {
        if (m_windowVisible)
        {
            CoreWindow::GetForCurrentThread()->Dispatcher->
                ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_main->Update();

            if (m_main->Render())
            {
                m_deviceResources->Present();
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread()->Dispatcher->
                ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }

    // The app is exiting so do the same thing as if the app were being suspended.
    m_main->OnSuspending();

#ifdef _DEBUG
    // Dump debug info when exiting.
    DumpD3DDebug();
#endif //_DEGBUG
}

狀態機器

遊戲通常包含狀態機器 (也稱為有限狀態機器或 FSM) 來控制遊戲邏輯的流程和順序。 狀態機器包含指定數目的狀態,以及在這些狀態之間轉換的能力。 狀態機器通常從初始狀態開始,轉換到一個或多個中間狀態,並且可能以最終狀態結束。

遊戲迴圈通常會使用狀態機器,以便執行目前遊戲狀態特有的邏輯。 Marble Maze 會定義 GameState 列舉,此列舉定義了遊戲的每種可能狀態。

enum class GameState
{
    Initial,
    MainMenu,
    HighScoreDisplay,
    PreGameCountdown,
    InGameActive,
    InGamePaused,
    PostGameResults,
};

例如,MainMenu 狀態會定義主功能表出現,且遊戲未啟動。 相反地,InGameActive 狀態會定義遊戲處於使用中狀態,且功能表不會出現。 MarbleMazeMain 類別會定義 m_gameState 成員變數來保存使用中遊戲狀態。

MarbleMazeMain::UpdateMarbleMazeMain::Render 方法使用 switch 語句來執行目前狀態的邏輯。 以下範例會顯示 MarbleMazeMain::Update 方法的 switch 語句可能會是什麼樣子 (移除了詳細資訊以說明結構)。

switch (m_gameState)
{
case GameState::MainMenu:
    // Do something with the main menu. 
    break;

case GameState::HighScoreDisplay:
    // Do something with the high-score table. 
    break;

case GameState::PostGameResults:
    // Do something with the game results. 
    break;

case GameState::InGamePaused:
    // Handle the paused state. 
    break;
}

當遊戲邏輯或轉譯取決於特定遊戲狀態時,我們會在此文件中強調。

處理應用程式和視窗事件

Windows 執行階段提供面向物件的事件處理系統,讓您更輕鬆地管理 Windows 訊息。 若要取用應用程式中的事件,您必須提供回應事件的事件處理常式或事件處理方法。 您也必須向事件來源註冊事件處理常式。 此程序通常稱為事件連接。

支援暫停、繼續和重新啟動

當使用者離開 Marble Maze 或 Windows 進入低電力狀態時,Marble Maze 會暫停。 當使用者將遊戲移動到前景或 Windows 退出低電力狀態時,遊戲將繼續。 一般而言,您不會關閉應用程式。 Windows 可以在處於暫停狀態時終止應用程式,而 Windows 需要應用程式所使用的資源,例如記憶體。 Windows 會在即將暫停或繼續時通知應用程式,但不會在即將終止時通知應用程式。 因此,您的應用程式必須能夠儲存—當 Windows 通知您的應用程式即將暫停時—任何在應用程式重新啟動時還原目前使用者狀態所需的資料。 如果您的應用程式有大量使用者狀態需要儲存,您可能也需要定期儲存狀態,甚至在您的應用程式收到暫停通知之前。 Marble Maze 會因下列兩個原因回應暫停和繼續通知:

  1. 當應用程式暫停時,遊戲會儲存目前的遊戲狀態,並暫停音訊播放。 當應用程式繼續時,遊戲會繼續播放音訊。
  2. 當應用程式關閉且稍後重新啟動時,遊戲會從先前的狀態繼續。

Marble Maze 會執行下列工作以支援暫停和繼續:

  • 它會將其狀態儲存至遊戲中關鍵點的永續性儲存體,例如當使用者到達檢查點時。
  • 它會將其狀態儲存至永續性儲存體,以回應暫停通知。
  • 它會從永續性儲存體載入其狀態,以回應繼續通知。 它也會在啟動期間載入先前的狀態。

為了支援暫停和繼續,Marble Maze 定義了 PersistentState 類別。 (請參閱 PersistentState.hPersistentState.cpp)。 此類別會使用 Windows::Foundation::Collections::IPropertySet 介面來讀取和寫入屬性。 PersistentState 類別會提供從備份存放區讀取和寫入原始資料類型 (例如 boolintfloatXMFLOAT3Platform::String) 的方法。

ref class PersistentState
{
internal:
    void Initialize(
        _In_ Windows::Foundation::Collections::IPropertySet^ settingsValues,
        _In_ Platform::String^ key
        );

    void SaveBool(Platform::String^ key, bool value);
    void SaveInt32(Platform::String^ key, int value);
    void SaveSingle(Platform::String^ key, float value);
    void SaveXMFLOAT3(Platform::String^ key, DirectX::XMFLOAT3 value);
    void SaveString(Platform::String^ key, Platform::String^ string);

    bool LoadBool(Platform::String^ key, bool defaultValue);
    int  LoadInt32(Platform::String^ key, int defaultValue);
    float LoadSingle(Platform::String^ key, float defaultValue);

    DirectX::XMFLOAT3 LoadXMFLOAT3(
        Platform::String^ key, 
        DirectX::XMFLOAT3 defaultValue);

    Platform::String^ LoadString(
        Platform::String^ key, 
        Platform::String^ defaultValue);

private:
    Platform::String^ m_keyName;
    Windows::Foundation::Collections::IPropertySet^ m_settingsValues;
};

MarbleMazeMain 類別保存一個 PersistentState 物件。 MarbleMazeMain 建構函式會初始化該物件,並提供本機應用程式資料存放區做為備份資料存放區。

m_persistentState = ref new PersistentState();

m_persistentState->Initialize(
    Windows::Storage::ApplicationData::Current->LocalSettings->Values,
    "MarbleMaze");

當彈珠經過檢查點或目標時 (在 MarbleMazeMain::Update 方法中) 以及當視窗失去焦點時 (在 MarbleMazeMain::OnFocusChange 方法中),Marble Maze 會儲存其狀態。 如果您的遊戲保存大量狀態資料,我們建議您偶爾以類似的方式將狀態保存到永續性儲存體,因為您只有幾秒鐘的時間來回應暫停通知。 因此,當您的應用程式收到暫停通知時,它只需要儲存已變更的狀態資料。

為了回應暫停和繼續通知,MarbleMazeMain 類別定義了在暫停和繼續時呼叫的 SaveStateLoadState 方法。 MarbleMazeMain::OnSuspending 方法會處理暫停事件,MarbleMazeMain::OnResuming 方法會處理繼續事件。

MarbleMazeMain::OnSuspending 方法會儲存遊戲狀態並暫停音訊。

void MarbleMazeMain::OnSuspending()
{
    SaveState();
    m_audio.SuspendAudio();
}

MarbleMazeMain::SaveState 方法會儲存遊戲狀態值,例如彈珠的目前位置和速度、最近的檢查點和高分資料表。

void MarbleMazeMain::SaveState()
{
    m_persistentState->SaveXMFLOAT3(":Position", m_physics.GetPosition());
    m_persistentState->SaveXMFLOAT3(":Velocity", m_physics.GetVelocity());

    m_persistentState->SaveSingle(
        ":ElapsedTime", 
        m_inGameStopwatchTimer.GetElapsedTime());

    m_persistentState->SaveInt32(":GameState", static_cast<int>(m_gameState));
    m_persistentState->SaveInt32(":Checkpoint", static_cast<int>(m_currentCheckpoint));

    int i = 0;
    HighScoreEntries entries = m_highScoreTable.GetEntries();
    const int bufferLength = 16;
    char16 str[bufferLength];

    m_persistentState->SaveInt32(":ScoreCount", static_cast<int>(entries.size()));

    for (auto iter = entries.begin(); iter != entries.end(); ++iter)
    {
        int len = swprintf_s(str, bufferLength, L"%d", i++);
        Platform::String^ string = ref new Platform::String(str, len);

        m_persistentState->SaveSingle(
            Platform::String::Concat(":ScoreTime", string), 
            iter->elapsedTime);

        m_persistentState->SaveString(
            Platform::String::Concat(":ScoreTag", string), 
            iter->tag);
    }
}

當遊戲繼續時,它只需要繼續音訊。 它不需要從永續性儲存體載入狀態,因為狀態已在記憶體中載入。

在 Marble Maze 範例中新增音訊文件中解釋了遊戲如何暫停和繼續音訊。

為了支援重新啟動,在啟動期間呼叫的 MarbleMazeMain 建構函式會呼叫 MarbleMazeMain::LoadState 方法。 MarbleMazeMain::LoadState 方法會讀取狀態並將其套用到遊戲物件。 如果遊戲暫停或使用中,這個方法也會將目前遊戲狀態設定為暫停。 我們暫停遊戲,以便使用者不會因意外活動而感到驚訝。 如果遊戲在暫停時沒有處於遊戲狀態,它也會移至主功能表。

void MarbleMazeMain::LoadState()
{
    XMFLOAT3 position = m_persistentState->LoadXMFLOAT3(
        ":Position", 
        m_physics.GetPosition());

    XMFLOAT3 velocity = m_persistentState->LoadXMFLOAT3(
        ":Velocity", 
        m_physics.GetVelocity());

    float elapsedTime = m_persistentState->LoadSingle(":ElapsedTime", 0.0f);

    int gameState = m_persistentState->LoadInt32(
        ":GameState", 
        static_cast<int>(m_gameState));

    int currentCheckpoint = m_persistentState->LoadInt32(
        ":Checkpoint", 
        static_cast<int>(m_currentCheckpoint));

    switch (static_cast<GameState>(gameState))
    {
    case GameState::Initial:
        break;

    case GameState::MainMenu:
    case GameState::HighScoreDisplay:
    case GameState::PreGameCountdown:
    case GameState::PostGameResults:
        SetGameState(GameState::MainMenu);
        break;

    case GameState::InGameActive:
    case GameState::InGamePaused:
        m_inGameStopwatchTimer.SetVisible(true);
        m_inGameStopwatchTimer.SetElapsedTime(elapsedTime);
        m_physics.SetPosition(position);
        m_physics.SetVelocity(velocity);
        m_currentCheckpoint = currentCheckpoint;
        SetGameState(GameState::InGamePaused);
        break;
    }

    int count = m_persistentState->LoadInt32(":ScoreCount", 0);

    const int bufferLength = 16;
    char16 str[bufferLength];

    for (int i = 0; i < count; i++)
    {
        HighScoreEntry entry;
        int len = swprintf_s(str, bufferLength, L"%d", i);
        Platform::String^ string = ref new Platform::String(str, len);

        entry.elapsedTime = m_persistentState->LoadSingle(
            Platform::String::Concat(":ScoreTime", string), 
            0.0f);

        entry.tag = m_persistentState->LoadString(
            Platform::String::Concat(":ScoreTag", string), 
            L"");

        m_highScoreTable.AddScoreToTable(entry);
    }
}

重要

Marble Maze 不會區分冷啟動,也就是說,從沒有先前暫停事件的第一次開始,並從暫停狀態繼續。 這是所有 UWP 應用程式的建議設計。

如需應用程式資料的更多資訊,請參閱存放與擷取設定和其他應用程式資料

下一步

請閱讀為 Marble Maze 範例新增視覺效果內容,以瞭解有關使用視覺資源時要記住的一些關鍵實務的資訊。