注释
本主题是 使用 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 运行时类型,通常可以完全避免指针,只需在堆栈上构造值。 如果需要指针,请使用 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 的实现。 加载 比 初始化 和 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();
}
我们在这里的唯一工作就是激活主要的 CoreWindow。 或者,可以选择在 App::SetWindow中执行此操作。
App::Run 方法
初始化、SetWindow和 加载 已做好准备。 游戏启动并运行后,将调用 IFrameworkView::Run 的实现。
void Run()
{
m_main->Run();
}
同样,工作被委托给 GameMain。
GameMain::Run 方法
GameMain::Run 是游戏的主要循环;可以在 GameMain.cpp
中找到它。 基本逻辑是,当游戏窗口保持打开状态时,调度所有事件,更新计时器,然后呈现和呈现图形管道的结果。 此外,还会调度和处理用于在游戏状态之间转换的事件。
此处的代码还涉及游戏引擎状态机中的两种状态。
- UpdateEngineState::Deactivated。 这说明游戏窗口已停用(失去焦点)或固定。
- UpdateEngineState: TooSmall。 指定的客户端区域太小,无法渲染游戏。
在这两种状态下,游戏会暂停事件处理,并等待窗口激活、退出固定状态或调整大小。
虽然游戏窗口可见(Window.Visibletrue
),但必须在消息队列到达时处理每个事件,因此必须使用 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 的实现。 这是我们进行整理的机会。 关闭应用窗口不会终止应用的进程;而是将应用单一实例的状态写入内存。 如果系统回收此内存时必须出现任何特殊情况(包括任何特殊的资源清理),请将该清理的代码放入 反初始化。
在本例中,App::Uninitialize 是 no-op。
void Uninitialize()
{
}
提示
开发自己的游戏时,围绕本主题中所述的方法设计启动代码。 下面是每个方法的基本建议的简单列表。
- 使用 初始化 来分配主类,并连接基本事件处理程序。
- 使用 SetWindow 来订阅任何窗口特定的事件,并将主窗口传递给设备相关的资源对象,以便在创建交换链时使用该窗口。
- 使用 加载 来处理任何剩余的设置,并启动对象的异步创建以及资源加载。 如果需要创建任何临时文件或数据(如过程生成的资产),也在此处执行此操作。
后续步骤
本主题介绍了使用 DirectX 的 UWP 游戏的一些基本结构。 记住这些方法是个好主意,因为我们将在后面的主题中回顾其中一些方法。
在下一个主题(游戏流管理)中,我们将深入探讨如何管理游戏状态和事件处理,以保持游戏流。