注释
本主题是 使用 DirectX 教程系列创建简单的通用 Windows 平台(UWP)游戏的一部分。 该链接中的主题设置序列的上下文。
游戏现在有一个窗口,已注册了一些事件处理程序,并且已异步加载资产。 本主题介绍如何使用游戏状态、如何管理特定的关键游戏状态,以及如何为游戏引擎创建更新循环。 然后,我们将了解用户界面流,最后,详细了解 UWP 游戏所需的事件处理程序。
用于管理游戏流程的游戏状态
我们使用游戏状态来管理游戏流。
当 Simple3DGameDX 示例游戏首次在计算机上运行时,它处于未开始任何游戏的状态。 游戏的后续运行时间,它可以处于上述任一状态。
- 游戏尚未开始,或者游戏正在关卡之间(高分为零)。
- 游戏循环正在运行,并且正在关卡的中间。
- 由于游戏已完成(高分具有非零值),游戏循环未运行。
你的游戏可以根据需要拥有任意数量的状态。 但请记住,它可以随时终止。 当它恢复时,用户期望它在终止时的状态中恢复。
游戏状态管理
因此,在游戏初始化期间,你需要支持冷启动游戏,并在停止游戏后恢复游戏。 Simple3DGameDX 示例始终保存其游戏状态,以便给人留下从未停止的印象。
为了响应暂停事件,游戏已暂停,但游戏的资源仍在内存中。 同样,处理恢复事件,以确保示例游戏能够从暂停或被关闭时的状态继续。 根据状态,向玩家显示不同的选项。
- 如果游戏在中途恢复,则它似乎暂停,叠加界面提供继续的选项。
- 如果游戏在完成后恢复,则会显示高分,并有一个选项来玩新游戏。
- 最后,如果在新的级别启动之前游戏恢复,界面会向用户提供一个开始选项。
示例游戏不区分游戏是冷启动、首次启动而不发生暂停事件,还是从暂停状态恢复。 这是任何 UWP 应用的正确设计。
在此示例中,游戏状态的初始化发生在 GameMain::InitializeGameState(下一部分显示了该方法的大纲)。
下面是一个流程图,可帮助你可视化流。 它涵盖初始化和更新循环。
- 在检查当前游戏状态时,初始化从 开始 节点开始。 有关游戏代码,请参阅下一部分中 GameMain::InitializeGameState。
- 有关更新循环的详细信息,请转到 更新游戏引擎。 要查看游戏代码,请转到 GameMain::Update。
的主状态机
GameMain::InitializeGameState 方法
GameMain::InitializeGameState 方法通过 GameMain 类的构造函数间接调用,这是 App::Load中创建 GameMain 实例的结果。
GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
m_deviceResources->RegisterDeviceNotify(this);
...
ConstructInBackground();
}
winrt::fire_and_forget GameMain::ConstructInBackground()
{
...
m_renderer->FinalizeCreateGameDeviceResources();
InitializeGameState();
...
}
void GameMain::InitializeGameState()
{
// Set up the initial state machine for handling Game playing state.
if (m_game->GameActive() && m_game->LevelActive())
{
// The last time the game terminated it was in the middle
// of a level.
// We are waiting for the user to continue the game.
...
}
else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
{
// The last time the game terminated the game had been completed.
// Show the high score.
// We are waiting for the user to acknowledge the high score and start a new game.
// The level resources for the first level will be loaded later.
...
}
else
{
// This is either the first time the game has run or
// the last time the game terminated the level was completed.
// We are waiting for the user to begin the next level.
...
}
m_uiControl->ShowGameInfoOverlay();
}
更新游戏引擎
App::Run 方法调用 GameMain::Run。 在 GameMain::Run 中,有一个基本的状态机,用于处理用户可以执行的所有主要操作。 这个状态机的最高层次处理游戏加载、进行特定关卡,或者在游戏被系统或用户暂停后继续某个关卡。
在示例游戏中,游戏可能处于 3 个主要状态(由 UpdateEngineState 枚举表示)之一。
- UpdateEngineState: WaitingForResources。 游戏循环正在进行,但在资源(特别是图形资源)可用之前,无法过渡到下一阶段。 异步资源加载任务完成后,我们将状态更新为 UpdateEngineState::ResourcesLoaded。 通常这种情况发生在关卡之间,当关卡从磁盘、游戏服务器或云后端加载新资源时。 在示例游戏中,我们模拟此行为,因为在那个阶段,示例不需要任何额外的分级资源。
- UpdateEngineState: WaitingForPress。 游戏循环正在运行,等待特定的用户输入。 此输入是玩家用于加载游戏、开始新关卡或继续当前关卡的操作。 示例代码通过 PressResultState 枚举来引用这些子状态。
- UpdateEngineState::Dynamics。 游戏循环在用户玩游戏时运行。 用户在玩游戏时,游戏会检查 3 个可以切换的条件:
- GameState::TimeExpired。 级别时间限制到期。
- GameState::LevelComplete。 玩家完成关卡。
- GameState::GameComplete。 玩家完成所有级别。
游戏只不过是一个包含多个较小状态机的状态机。 每个特定状态都必须由非常具体的条件定义。 从一种状态转换到另一个状态必须基于离散用户输入或系统操作(如图形资源加载)。
在规划游戏时,请考虑绘制整个游戏流,以确保你已解决用户或系统可以采取的所有可能操作。 游戏可能非常复杂,因此状态机是一种功能强大的工具,可帮助你直观显示这种复杂性,并使其更易于管理。
让我们看看更新循环的代码。
GameMain::Update 方法
这是用于更新游戏引擎的状态机的结构。
void GameMain::Update()
{
// The controller object has its own update loop.
m_controller->Update();
switch (m_updateState)
{
case UpdateEngineState::WaitingForResources:
...
break;
case UpdateEngineState::ResourcesLoaded:
...
break;
case UpdateEngineState::WaitingForPress:
if (m_controller->IsPressComplete())
{
...
}
break;
case UpdateEngineState::Dynamics:
if (m_controller->IsPauseRequested())
{
...
}
else
{
// When the player is playing, work is done by Simple3DGame::RunGame.
GameState runState = m_game->RunGame();
switch (runState)
{
case GameState::TimeExpired:
...
break;
case GameState::LevelComplete:
...
break;
case GameState::GameComplete:
...
break;
}
}
if (m_updateState == UpdateEngineState::WaitingForPress)
{
// Transitioning state, so enable waiting for the press event.
m_controller->WaitForPress(
m_renderer->GameInfoOverlayUpperLeft(),
m_renderer->GameInfoOverlayLowerRight());
}
if (m_updateState == UpdateEngineState::WaitingForResources)
{
// Transitioning state, so shut down the input controller
// until resources are loaded.
m_controller->Active(false);
}
break;
}
}
更新用户界面
我们需要让玩家了解系统的状态,并允许游戏状态根据玩家的操作和定义游戏的规则而更改。 许多游戏(包括此示例游戏)通常使用用户界面(UI)元素向玩家显示此信息。 UI 包含游戏状态和其他特定于游戏的信息(例如分数、弹药或剩余机会数)的表示形式。 UI 也称为覆盖层,因为它与主图形管道分开呈现,并放置在 3D 投影的顶部。
一些用户界面信息会通过抬头显示器(HUD)展示,使用户无需完全移开视线即可查看这些信息,从而专注于主要游戏区域。 在示例游戏中,我们使用 Direct2D API 创建此覆盖层。 或者,我们可以使用 XAML 创建这个叠加层,我们将在 扩展样本游戏中讨论。
用户界面有两个组件。
- 包含有关当前游戏状态的分数和信息的 HUD。
- 暂停位图,它是一个黑色矩形,在游戏的暂停/暂停状态期间覆盖文本。 这是游戏界面覆盖层。 我们将在 添加用户界面中进一步讨论。
毫不奇怪,覆盖层也有状态机。 覆盖可以显示关卡开始或游戏结束的消息。 它本质上是一个画布,在游戏暂停或挂起时,我们可以在其上输出任何有关游戏状态的信息以显示给玩家。
渲染的覆盖界面可以是以下六个屏幕之一,具体取决于游戏的状态。
- 游戏开始时的资源加载进度屏幕。
- 游戏统计信息屏幕。
- 关卡启动提示屏幕。
- 完成所有关卡且时间未耗尽时的游戏结束画面。
- 时间用完时,游戏结束屏幕。
- 暂停菜单屏幕。
将用户界面与游戏的图形管道分离后,你可以独立于游戏的图形呈现引擎来处理它,并显著降低游戏代码的复杂性。
这里是示例游戏如何构建界面叠加的状态机。
void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
m_gameInfoOverlayState = state;
switch (state)
{
case GameInfoOverlayState::Loading:
m_uiControl->SetGameLoading(m_loadingCount);
break;
case GameInfoOverlayState::GameStats:
...
break;
case GameInfoOverlayState::LevelStart:
...
break;
case GameInfoOverlayState::GameOverCompleted:
...
break;
case GameInfoOverlayState::GameOverExpired:
...
break;
case GameInfoOverlayState::Pause:
...
break;
}
}
事件处理
正如我们在 定义游戏的 UWP 应用框架 主题中看到的那样,应用 类的许多视图提供程序方法都注册了事件处理程序。 在添加游戏机制或启动图形开发之前,这些方法需要正确处理这些重要事件。
正确处理相关事件对于 UWP 应用体验至关重要。 由于 UWP 应用可以在任何时候被激活、停用、调整大小、贴靠、取消贴靠、暂停或恢复,因此游戏必须尽快注册这些事件,并以一种能够保持玩家体验流畅且可预测的方式处理它们。
这些是此示例中使用的事件处理程序,以及它们处理的事件。
事件处理程序 | DESCRIPTION |
---|---|
OnActivated | 处理 CoreApplicationView::Activated。 游戏应用程序已移到前台,因此主窗口已激活。 |
OnDpiChanged | 处理 Graphics::Display::DisplayInformation::DpiChanged。 显示器的 DPI 已更改,游戏会相应地调整其资源。
注意,CoreWindow 坐标采用与设备无关的像素(DIP),Direct2D。 因此,必须通知 Direct2D DPI 中的更改,才能正确显示任何 2D 资产或基元。
|
OnOrientationChanged | 处理 Graphics::Display::DisplayInformation::OrientationChanged。 显示的方向发生变化,因此需要更新渲染。 |
显示内容已失效 | 处理 Graphics::Display::DisplayInformation::DisplayContentsInvalidated。 显示需要重新绘制,并且游戏需要再次渲染。 |
OnResuming | 处理 CoreApplication::Resuming。 游戏应用程序从挂起状态恢复游戏。 |
暂停中 | 处理 CoreApplication::Suspending。 游戏应用将其状态保存到磁盘。 它有 5 秒钟的时间来将状态保存到存储中。 |
当可见性改变时 | 处理 CoreWindow::VisibilityChanged。 游戏应用的可见性已更改,变为可见或由于另一个应用变得可见而不可见。 |
窗口激活状态改变时 | 处理 CoreWindow::Activated。 游戏应用的主窗口已停用或激活,因此它必须删除焦点并暂停游戏,或重新获得焦点。 在这两种情况下,覆盖层都表示游戏已暂停。 |
窗口关闭时 | 处理 CoreWindow::Closed。 游戏应用关闭主窗口并挂起游戏。 |
OnWindowSizeChanged | 处理 CoreWindow::SizeChanged。 游戏应用重新分配图形资源和覆盖以容纳大小更改,然后更新呈现目标。 |
后续步骤
在本主题中,我们了解了如何使用游戏状态管理整个游戏流,并且游戏由多个不同的状态机组成。 我们还了解了如何更新 UI 和管理关键应用事件处理程序。 现在,我们已准备好深入了解呈现循环、游戏及其机制。
你可以按任意顺序浏览记录此游戏的剩余主题。