定义游戏的 UWP 应用框架
注意
本主题是使用 DirectX 创建简单的通用 Windows 平台 (UWP) 游戏教程系列的一部分。 此链接上的主题设置了该系列的上下文。
对通用 Windows 平台 (UWP) 游戏进行编码的第一步是构建框架,使应用对象可以与 Windows 交互,包括 Windows 运行时功能(如暂停-恢复事件处理、窗口可见性的更改以及贴靠)。
目标
- 为通用 Windows 平台 (UWP) DirectX 游戏设置框架,以及实现定义整个游戏流的状态机。
注意
若要继续本主题,请查看下载的 Simple3DGameDX 示例游戏的源代码。
简介
在设置游戏项目主题中,我们介绍了 wWinMain 函数以及 IFrameworkViewSource 和 IFrameworkView 接口。 我们已了解到 App 类(可以看到是在 Simple3DGameDX 项目的 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 运行时类型,通常可以完全避免指针,而只在堆栈上构造值。 如果确实需要指针,请使用 winrt::com_ptr(我们很快会看到该指针的示例)。
- 对于唯一指针,请使用 std::unique_ptr 和 std::make_unique。
- 对于共享指针,请使用 std::shared_ptr 和 std::make_shared。
App::SetWindow 方法
执行 Initialize 后,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 的实现。 与 SetWindow 或 Initialize 相比,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 控件对象和显示游戏信息覆盖层以在加载资源文件时显示进度栏。 有关详细信息,请参阅添加用户界面。
- 创建控制器对象,以从控制器(触控装置、鼠标或 Xbox 游戏控制器)读取输入。 有关详细信息,请参阅添加控件。
- 在屏幕的左下角和右下角定义两个矩形区域,分别用于移动和相机触摸控件。 玩家会将左下角的矩形(在 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 为 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 游戏的一些基本结构。 最好记住这些方法,因为我们将在后面的主题中回顾其中一些方法。
在下一个主题(游戏流管理)中,我们将深入了解如何管理游戏状态和事件处理以便让游戏顺畅进行。