定義遊戲的 UWP 應用程式架構
注意
本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。
撰寫通用 Windows 平台 (UWP) 遊戲程式碼的第一步是建置架構,讓應用程式物件與 Windows 互動,包括 Windows 執行階段功能,例如:暫止-繼續事件處理、視窗可見度變更,以及貼齊。
目標
- 設定通用 Windows 平台 (UWP) DirectX 遊戲的架構,並實作用於定義整體遊戲流程的狀態機器。
注意
如要閱讀本主題內容,請對照您下載的 Simple3DGameDX 範例遊戲原始程式碼。
簡介
在<設定遊戲專案>主題中,我們介紹了wWinMain 函式,以及 IFrameworkViewSource 和 IFrameworkView 介面。 我們已經知道 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_ptr 和 std::make_unique。
- 對於共用指標,請使用 std::shared_ptr 和 std::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。 比起 Initialize 和 SetWindow,Load 更適合用來預先提取遊戲資料或資產。
void Load(winrt::hstring const& /* entryPoint */)
{
if (!m_main)
{
m_main = winrt::make_self<GameMain>(m_deviceResources);
}
}
如您所見,實際工作會委派給 GameMain 物件的建構函式 (該物件是我們在這裡建立的)。 GameMain 類別定義於 GameMain.h
和 GameMain.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::LoadLevelAsync 和 Simple3DGame::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 方法
Initialize、SetWindow 和 Load 已完成準備。 遊戲已啟動且開始執行,並呼叫我們實作的 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.Visible 為 false
)、已暫止遊戲或太小 (已貼齊),您應該不希望遊戲再耗用任何資源迴圈分派永遠不會送達的訊息。 在此情況下,遊戲必須使用 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 遊戲的一些基本結構。 建議記下這些方法,我們後續的主題會再提到。
在下一個主題<遊戲流程管理>中,我們將深入探討如何管理遊戲狀態和事件處理,以保持遊戲順暢執行。