備註
本主題是 使用 DirectX 教學課程系列建立簡單的通用 Windows 平臺 (UWP) 遊戲的一部分。 該連結中的主題設定了整個系列的背景。
撰寫通用 Windows 平台(UWP)遊戲程式碼的第一個步驟是建立一個框架,讓應用程式物件能夠與 Windows 互動,包括 Windows 執行階段功能,例如暫停與繼續事件的處理、視窗可見度變更,以及視窗對接。
目標
- 設定通用 Windows 平臺 (UWP) DirectX 遊戲的架構,並實作定義整體遊戲流程的狀態機器。
備註
若要遵循本主題,請查看您下載之 Simple3DGameDX 範例遊戲的原始程式碼。
簡介
在 設定遊戲專案 主題中,我們介紹了 wWinMain 函數,以及 IFrameworkViewSource 和 IFrameworkView 介面。 我們瞭解到,App 類別(您可以在 App.cpp 專案中的 原始碼檔案中看到定義)同時作為 視圖提供者工廠 及 視圖提供者。
本主題會從該處接續,並進一步詳述遊戲中的 App 類別應該如何實作 IFrameworkView的方法。
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 Runtime 類型,您通常可以完全避免使用指標,只需在堆疊上建立一個值。 如果您需要指標,請使用 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 的實作。 Load 比 Initialize 和 SetWindow更適合用來預取遊戲數據或資產。
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();
}
我們在此的唯一工作是啟用主要的 核心視窗。 或者,您可以選擇在 App::SetWindow 中執行此動作。
App::Run 方法
初始化、SetWindow和 Load 已做好準備。 現在遊戲已順利運行,我們實作 IFrameworkView::Run。
void Run()
{
m_main->Run();
}
同樣地,工作會委派給 GameMain。
GameMain::Run 方法
GameMain::Run 是遊戲的核心迴圈,您可以在GameMain.cpp中找到它。 基本邏輯是,當遊戲的視窗保持開啟時,請分派所有事件、更新定時器,然後轉譯並呈現圖形管線的結果。 此外,在這裡,用來在遊戲狀態之間轉換的事件會分派和處理。
這裡的程式代碼也與遊戲引擎狀態機器中的兩個狀態有關。
- UpdateEngineState: Deactivated。 這表示遊戲視窗已被停用(失去焦點)或已依附到螢幕邊緣。
- UpdateEngineState::TooSmall。 這表示客戶端區域太小,無法渲染遊戲。
在上述任一狀態中,遊戲會暫停事件處理,並等候視窗啟動、解除對齊或調整大小。
當您的遊戲視窗可見時(Window.Visible 是 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 是 no-op。
void Uninitialize()
{
}
提示
開發自己的遊戲時,請圍繞本主題所述的方法設計啟動程序代碼。 以下是每個方法的基本建議的簡單清單。
- 使用 Initialize 來配置主要類別,並連接基本事件處理程式。
- 使用 SetWindow 訂閱任何視窗特定事件,將您的主視窗傳遞給裝置相依資源物件,以便在建立交換鏈時使用該視窗。
- 使用 Load 來處理任何剩餘的設定,並起始物件的異步建立和資源的載入。 如果您需要建立任何暫時性檔案或資料,例如程序生成的資產,也請在此完成。
後續步驟
本主題涵蓋使用 DirectX 之 UWP 遊戲的一些基本結構。 記住這些方法是個好主意,因為我們將在稍後的主題中回顧其中一些方法。
在下一個主題-遊戲流程管理中,我們將深入探討如何管理遊戲狀態和事件處理,以保持遊戲流動。