定義遊戲的 UWP 應用程式架構

注意

本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。

撰寫通用 Windows 平台 (UWP) 遊戲程式碼的第一步是建置架構,讓應用程式物件與 Windows 互動,包括 Windows 執行階段功能,例如:暫止-繼續事件處理、視窗可見度變更,以及貼齊。

目標

  • 設定通用 Windows 平台 (UWP) DirectX 遊戲的架構,並實作用於定義整體遊戲流程的狀態機器。

注意

如要閱讀本主題內容,請對照您下載的 Simple3DGameDX 範例遊戲原始程式碼。

簡介

在<設定遊戲專案>主題中,我們介紹了wWinMain 函式,以及 IFrameworkViewSourceIFrameworkView 介面。 我們已經知道 App 類別 (已於 Simple3DGameDX 專案的 App.cpp 原始程式碼中定義) 可作為 view-provider factory,亦可當作 view-provider

本主題會接續該內容,進一步詳細說明遊戲中的 App 類別應如何實作 IFrameworkView 方法。

The App::Initialize 方法

應用程式啟動時,Windows 呼叫的第一個方法是我們實作的 IFrameworkView::Initialize

實作應處理 UWP 遊戲最基本的行為,例如:透過訂閱這類事件,確保遊戲可處理暫止事件 (以及稍後可能發生的繼續事件)。 我們也可以在這裡存取顯示卡裝置,因此可建立相依於裝置的圖形資源。

void Initialize(CoreApplicationView const& applicationView)
{
    applicationView.Activated({ this, &App::OnActivated });

    CoreApplication::Suspending({ this, &App::OnSuspending });

    CoreApplication::Resuming({ this, &App::OnResuming });

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

盡可能避免原始指標 (幾乎一律可行)。

  • 若為 Windows 執行階段類別,通常可完全避免使用指標,而只是在堆疊上建構值。 若需要指標,請使用 winrt::com_ptr (很快就會看到範例)。
  • 對於唯一指標,請使用 std::unique_ptrstd::make_unique
  • 對於共用指標,請使用 std::shared_ptrstd::make_shared

App::SetWindow 方法

初始化之後,Windows 會呼叫我們實作的 IFrameworkView::SetWindow,並傳遞 CoreWindow 代表遊戲主視窗的物件。

App::SetWindow 中,我們會訂閱視窗相關事件,並設定一些視窗和顯示行為。 例如,我們會建構滑鼠指標 (透過 CoreCursor 類別),滑鼠和觸控控制項皆可使用該指標。 我們也會將視窗物件傳遞至與裝置相依的資源物件。

我們會在<遊戲流程管理>主題中說明更多有關處理事件的內容。

void SetWindow(CoreWindow const& window)
{
    //CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();

    window.PointerCursor(CoreCursor(CoreCursorType::Arrow, 0));

    PointerVisualizationSettings visualizationSettings{ PointerVisualizationSettings::GetForCurrentView() };
    visualizationSettings.IsContactFeedbackEnabled(false);
    visualizationSettings.IsBarrelButtonFeedbackEnabled(false);

    m_deviceResources->SetWindow(window);

    window.Activated({ this, &App::OnWindowActivationChanged });

    window.SizeChanged({ this, &App::OnWindowSizeChanged });

    window.Closed({ this, &App::OnWindowClosed });

    window.VisibilityChanged({ this, &App::OnVisibilityChanged });

    DisplayInformation currentDisplayInformation{ DisplayInformation::GetForCurrentView() };

    currentDisplayInformation.DpiChanged({ this, &App::OnDpiChanged });

    currentDisplayInformation.OrientationChanged({ this, &App::OnOrientationChanged });

    currentDisplayInformation.StereoEnabledChanged({ this, &App::OnStereoEnabledChanged });

    DisplayInformation::DisplayContentsInvalidated({ this, &App::OnDisplayContentsInvalidated });
}

App::Load 方法

主視窗已設定完成,接著要呼叫我們實作的 IFrameworkView::Load。 比起 InitializeSetWindowLoad 更適合用來預先提取遊戲資料或資產。

void Load(winrt::hstring const& /* entryPoint */)
{
    if (!m_main)
    {
        m_main = winrt::make_self<GameMain>(m_deviceResources);
    }
}

如您所見,實際工作會委派給 GameMain 物件的建構函式 (該物件是我們在這裡建立的)。 GameMain 類別定義於 GameMain.hGameMain.cpp

GameMain::GameMain 建構函式

GameMain 建構函式 (及其呼叫的其他成員函式) 會開始一組非同步載入作業,以建立遊戲物件、載入圖形資源,以及初始化遊戲的狀態機器。 我們也要在遊戲開始前進行任何必要的準備,例如:設定起始狀態或全域值。

Windows 可對遊戲施加時間限制,規定其開始處理輸入之前可等待多久。 因此,使用非同步作業 (即此處案例) 表示 Load 可快速傳回結果,而其開始的工作可同時在背景中持續執行。 如果載入會花很久時間,或資源很多,不妨為使用者提供頻繁更新的進度列。

如果您不熟悉非同步程式設計,請參閱<透過 C++/WinRT 的並行和非同步作業>。

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) :
    m_deviceResources(deviceResources),
    m_windowClosed(false),
    m_haveFocus(false),
    m_gameInfoOverlayCommand(GameInfoOverlayCommand::None),
    m_visible(true),
    m_loadingCount(0),
    m_updateState(UpdateEngineState::WaitingForResources)
{
    m_deviceResources->RegisterDeviceNotify(this);

    m_renderer = std::make_shared<GameRenderer>(m_deviceResources);
    m_game = std::make_shared<Simple3DGame>();

    m_uiControl = m_renderer->GameUIControl();

    m_controller = std::make_shared<MoveLookController>(CoreWindow::GetForCurrentThread());

    auto bounds = m_deviceResources->GetLogicalSize();

    m_controller->SetMoveRect(
        XMFLOAT2(0.0f, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(GameUIConstants::TouchRectangleSize, bounds.Height)
        );
    m_controller->SetFireRect(
        XMFLOAT2(bounds.Width - GameUIConstants::TouchRectangleSize, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(bounds.Width, bounds.Height)
        );

    SetGameInfoOverlay(GameInfoOverlayState::Loading);
    m_uiControl->SetAction(GameInfoOverlayCommand::None);
    m_uiControl->ShowGameInfoOverlay();

    // Asynchronously initialize the game class and load the renderer device resources.
    // By doing all this asynchronously, the game gets to its main loop more quickly
    // and in parallel all the necessary resources are loaded on other threads.
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    auto lifetime = get_strong();

    m_game->Initialize(m_controller, m_renderer);

    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);

    // The finalize code needs to run in the same thread context
    // as the m_renderer object was created because the D3D device context
    // can ONLY be accessed on a single thread.
    // co_await of an IAsyncAction resumes in the same thread context.
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();

    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        // In the middle of a game so spin up the async task to load the level.
        co_await m_game->LoadLevelAsync();

        // The m_game object may need to deal with D3D device context work so
        // again the finalize code needs to run in the same thread
        // context as the m_renderer object was created because the D3D
        // device context can ONLY be accessed on a single thread.
        m_game->FinalizeLoadLevel();
        m_game->SetCurrentLevelToSavedState();
        m_updateState = UpdateEngineState::ResourcesLoaded;
    }
    else
    {
        // The game is not in the middle of a level so there aren't any level
        // resources to load.
    }

    // Since Game loading is an async task, the app visual state
    // may be too small or not be activated. Put the state machine
    // into the correct state to reflect these cases.

    if (m_deviceResources->GetLogicalSize().Width < GameUIConstants::MinPlayableWidth)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::TooSmall;
        m_controller->Active(false);
        m_uiControl->HideGameInfoOverlay();
        m_uiControl->ShowTooSmall();
        m_renderNeeded = true;
    }
    else if (!m_haveFocus)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::Deactivated;
        m_controller->Active(false);
        m_uiControl->SetAction(GameInfoOverlayCommand::None);
        m_renderNeeded = true;
    }
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    ...
}

以下概要列出建構函式啟動的工作順序。

  • 建立並初始化 GameRenderer 類型的物件。 如需詳細資訊,請參閱<轉譯架構 I:轉譯簡介>。
  • 建立並初始化 Simple3DGame 類型的物件。 如需詳細資訊,請參閱<定義主要遊戲物件>。
  • 建立遊戲 UI 控制項物件,並顯示遊戲資訊重疊,以在資源檔案載入時顯示進度列。 如需詳細資訊,請參閱<新增使用者介面>。
  • 建立控制項物件,以從控制器 (觸控、滑鼠或遊戲控制器) 讀取輸入。 如需詳細資訊,請參閱<新增控制項>。
  • 針對移動和相機觸控控制項,分別定義螢幕左下角和右下角的兩個矩形區域。 玩家會使用左下角矩形 (在 SetMoveRect 呼叫中定義) 作為虛擬控制台,以前後左右移動相機。 右下角矩形 (由 SetFireRect 方法定義) 作為觸發彈藥的虛擬按鈕。
  • 使用協同程式將資源載入劃分成不同階段。 若要存取 Direct3D 裝置內容,僅限於在裝置內容建立時所在的執行緒,但可在任何執行緒存取 Direct3D 裝置以建立物件。 因此,GameRenderer::CreateGameDeviceResourcesAsync 協同程式能在另一個執行緒執行,獨立於完成工作 GameRenderer::FinalizeCreateGameDeviceResources (在原始執行緒上執行的)。
  • 我們採取類似的模式,以 Simple3DGame::LoadLevelAsyncSimple3DGame::FinalizeLoadLevel 載入關卡資源。

我們會在下一個主題 (遊戲流程管理) 進一步介紹 GameMain::InitializeGameState

App::OnActivated 方法

接著,請引發 CoreApplicationView::Activated 事件。 因此,系統會呼叫您擁有的任何 OnActivated 事件處理常式 (例如 App::OnActivated 方法)。

void OnActivated(CoreApplicationView const& /* applicationView */, IActivatedEventArgs const& /* args */)
{
    CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();
}

此時我們唯一的工作是啟用主要 CoreWindow。 或者,您可選擇在 App::SetWindow 中執行。

App::Run 方法

InitializeSetWindowLoad 已完成準備。 遊戲已啟動且開始執行,並呼叫我們實作的 IFrameworkView::Run

void Run()
{
    m_main->Run();
}

同樣地,工作會委派給 GameMain

GameMain::Run 方法

GameMain::Run 是遊戲的主要迴圈,可在 GameMain.cpp 中找到。 基本邏輯是遊戲的視窗保持開啟時,則分派所有事件、更新定時器,然後轉譯並呈現圖形管線的結果。 在此情況下,也要分派和處理用於轉換遊戲狀態的事件。

這裡的程式碼也與遊戲引擎狀態機器中的兩個狀態有關。

  • UpdateEngineState::Deactivated。 這會指定遊戲視窗已停用 (失去焦點) 或已貼齊。
  • UpdateEngineState::TooSmall。 這會指定工作區太小,無法轉譯遊戲。

在上述任一狀態中,遊戲會暫止事件處理,並等候視窗啟用、取消貼齊或調整大小。

如果遊戲視窗為可見狀態 (Window.Visible is true),您必須處理訊息佇列中的每個事件,因此必須使用 ProcessAllIfPresent 選項呼叫 CoreWindowDispatch.ProcessEvents。 若使用其他選項,可能會導致訊息事件處理延遲,而使遊戲感覺上無回應或觸控行為變得緩慢。

如果遊戲「不」可見 (Window.Visiblefalse)、已暫止遊戲或太小 (已貼齊),您應該不希望遊戲再耗用任何資源迴圈分派永遠不會送達的訊息。 在此情況下,遊戲必須使用 ProcessOneAndAllPending 選項。 該選項會執行封鎖,直到收到事件,接著才處理該事件 (以及第一次處理期間排入佇列的任何其他事件)。 CoreWindowDispatch.ProcessEvents 會在佇列處理完成後立即傳回。

請見下方範例程式碼,m_visible 資料成員表示視窗的可見度。 遊戲暫止時,其視窗不會顯示。 視窗「可見」時,m_updateState (UpdateEngineState 列舉) 值進一步判斷視窗是否已停用 (失去焦點)、太小 (已貼齊) 或是適當的大小。

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                if (m_updateStateNext == UpdateEngineState::WaitingForResources)
                {
                    WaitingForResourceLoading();
                    m_renderNeeded = true;
                }
                else if (m_updateStateNext == UpdateEngineState::ResourcesLoaded)
                {
                    // In the device lost case, we transition to the final waiting state
                    // and make sure the display is updated.
                    switch (m_pressResult)
                    {
                    case PressResultState::LoadGame:
                        SetGameInfoOverlay(GameInfoOverlayState::GameStats);
                        break;

                    case PressResultState::PlayLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::LevelStart);
                        break;

                    case PressResultState::ContinueLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::Pause);
                        break;
                    }
                    m_updateStateNext = UpdateEngineState::WaitingForPress;
                    m_uiControl->ShowGameInfoOverlay();
                    m_renderNeeded = true;
                }

                if (!m_renderNeeded)
                {
                    // The App is not currently the active window and not in a transient state so just wait for events.
                    CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
                    break;
                }
                // otherwise fall through and do normal processing to get the rendering handled.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                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.
}

App::Uninitialize 方法

遊戲結束時,呼叫我們實作的 IFrameworkView::Uninitialize。 這是執行清除的時機。 關閉應用程式視窗並不會終止應用程式程序,只會將應用程式單一狀態寫入記憶體。 如果系統回收此記憶體時,一定要發生某種特殊情況 (包括清除資源),請將該清除作業的程式碼置入 Uninitialize

在此案例中,App::Uninitialize 為不作業。

void Uninitialize()
{
}

提示

開發自己的遊戲時,請按照本主題所述的方法設計啟動程式碼。 以下簡單列出各方法的基本建議。

  • 使用 Initialize 配置主要類別,並連結基本事件處理常式。
  • 使用 SetWindow 訂閱任何視窗特定事件,並將主視窗傳遞至裝置相依資源物件,以在建立交換鏈結時使用該視窗。
  • 使用 Load 處理任何其餘設定、啟動物件的非同步建立作業,以及載入資源。 如果需要建立任何暫存檔案或資料 (例如以程序方式產生的資產),請在這裡執行。

下一步

本主題已說明使用 DirectX 建立 UWP 遊戲的一些基本結構。 建議記下這些方法,我們後續的主題會再提到。

在下一個主題<遊戲流程管理>中,我們將深入探討如何管理遊戲狀態和事件處理,以保持遊戲順暢執行。