游戏流管理

注意

本主题是使用 DirectX 创建简单的通用 Windows 平台 (UWP) 游戏教程系列的一部分。 此链接上的主题设置了该系列的上下文。

游戏现在具有一个窗口,已注册一些事件处理程序并异步加载了资产。 此部分介绍游戏状态的使用、如何管理特定的关键游戏状态以及如何创建游戏引擎的更新循环。 然后,我们将了解用户界面流,最后了解 UWP 游戏所需的事件处理程序的详细信息。

用于管理游戏流的游戏状态

我们使用游戏状态管理游戏流。

当 Simple3DGameDX 示例游戏在计算机上首次运行时,它处于未启动任何游戏的状态。 该游戏后续运行时,可以处于这些状态中的任何一种。

  • 没有启动游戏或者游戏在两个级别之间(高分为零)。
  • 游戏循环正在运行并处于某个级别的中间。
  • 由于游戏刚刚完成(高分具有非零值),游戏循环未运行。

游戏可以具有所需数量的状态。 但请记住,它可以随时终止。 进行恢复时,用户期望它以终止时所处的状态恢复。

游戏状态管理

因此,在游戏初始化期间,你需要支持冷启动游戏,以及在停止运行的游戏后恢复游戏。 Simple3DGameDX 示例始终保存其游戏状态,以便给人留下从不停止的印象。

为响应暂停事件,游戏运行暂停,但游戏的资源仍然在内存中。 同样,处理恢复事件是为了确保该示例游戏拾取其上次暂停或终止时所处的状态。 根据游戏的状态,为玩家提供不同的选项。

  • 如果游戏在级别中间恢复,游戏将显示为暂停,覆盖层将提供用于继续的选项。
  • 如果游戏恢复到游戏已经完成的状态,它将显示高分和一个玩新游戏的选项。
  • 最后,如果游戏在某个级别开始之前恢复,则覆盖层为用户提供一个开始选项。

该示例游戏不区分游戏是在冷启动(即首次启动且无暂停事件)或是从暂停状态恢复。 这是任何 UWP 应用的正确设计。

在此示例中,游戏状态的初始化发生在 GameMain::InitializeGameState 中(下一部分中会演示该方法的概述)。

下面是一个流程图,可帮助将该流程可视化。 它包含初始化和更新循环。

  • 初始化从你检查当前游戏状态的开始节点开始。 有关游戏代码,请参阅下一部分中的 GameMain::InitializeGameState

游戏的主状态机

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 枚举进行表示)。

  1. UpdateEngineState::WaitingForResources。 游戏循环正在执行,在资源(特别是图形资源)可用之前无法转换。 异步资源加载任务完成后,我们将状态更新为 UpdateEngineState::ResourcesLoaded。 当关卡正在从磁盘、游戏服务器或云后端加载新资源时,通常会在关卡之间发生此状态。 在该示例游戏中,将模拟此行为,因为该示例此时不需要任何附加的每关卡资源。
  2. UpdateEngineState::WaitingForPress。 游戏循环正在执行,正在等待特定的用户输入。 此输入是玩家加载游戏、开始某个级别或继续某个级别的操作。 示例代码通过 PressResultState 枚举引用这些子状态。
  3. UpdateEngineState::Dynamics。 在用户玩游戏过程中游戏循环正在运行。 在用户游戏过程中,游戏检查其可以转换的 3 个条件:
  • GameState::TimeExpired。 某个级别的时间限制到期。
  • GameState::LevelComplete。 玩家完成某个级别。
  • GameState::GameComplete。 玩家完成所有级别。

游戏只是一个含有多个较小状态机的状态机。 每个特定状态都必须使用非常具体的条件进行定义。 从一种状态到另一状态的转换必须基于离散用户的输入或系统操作(如图形资源加载)。

规划游戏时,考虑绘制整个游戏流,以确保涵盖用户或系统可能采取的所有操作。 游戏可能非常复杂,因此状态机是一个强大的工具,它有助于直观显示这一复杂性,并使之非常易于管理。

让我们来看一看更新循环代码。

The 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 投影的顶部。

一些 UI 信息还作为提醒显示 (HUD) 呈现,使用户的视线无需离开主游戏区也可看到该信息。 在该示例游戏中,我们使用 Direct2D API 创建此覆盖层。 或者,我们可以使用 XAML 创建此覆盖层,这在扩展示例游戏中讨论。

用户界面有两个组件。

  • HUD,包含得分和有关游戏执行的当前状态信息。
  • 暂停位图,这是一个在游戏的暂停状态期间由文本覆盖的黑色矩形。 这是游戏覆盖层。 我们将在添加用户界面中进一步讨论。

毫不奇怪,覆盖层也有一个状态机。 覆盖层可以显示一个级别的开始或游戏结束消息。 它实际上是一块画布,在游戏暂停时,我们可以在其上输出要向玩家显示的有关游戏状态的任何信息。

呈现的覆盖层可以是这六个屏幕中的其中一个,具体取决于游戏的状态。

  1. 游戏开始时的资源加载进度屏幕。
  2. 游戏统计信息屏幕。
  3. 级别开始状态屏幕。
  4. 超时前完成所有级别时的游戏结束屏幕。
  5. 超时后的游戏结束屏幕。
  6. 暂停菜单屏幕。

将用户界面与游戏的图形管道分开可以独立于游戏的图形呈现引擎来使用用户界面,并显著降低游戏代码的复杂性。

下面是示例游戏构造覆盖层状态机的方式。

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 应用框架主题中所看到的,App 类的许多视图提供程序方法都注册事件处理程序。 在添加游戏构成或开始图形开发之前,这些方法需要正确处理这些重要事件。

正确处理有关事件是 UWP 应用体验的基础。 由于 UWP 应用可随时激活、停用、调整大小、贴靠、取消贴靠、暂停或恢复,因此游戏必须尽快注册这些事件,并且处理这些事件的方式应保证玩家获得流畅和可预期的体验。

下面是此示例中的事件处理程序,以及它们处理的事件。

事件处理程序 说明
OnActivated 处理 CoreApplicationView::Activated。 游戏应用已经进入前台,因此将激活主窗口。
OnDpiChanged 处理 Graphics::Display::DisplayInformation::DpiChanged。 显示器的 DPI 已更改,且游戏相应地调整其资源。
NoteCoreWindow 坐标以适用于 Direct2D 的与设备无关的像素 (DIP) 为单位。 因此,要正确显示任何 2D 资产或基元,必须通知 Direct2D 关于 DPI 的更改。
OnOrientationChanged 处理 Graphics::Display::DisplayInformation::OrientationChanged。 显示方向更改,并且需要更新呈现。
OnDisplayContentsInvalidated 处理 Graphics::Display::DisplayInformation::DisplayContentsInvalidated。 需要重新绘制显示,且需要再次呈现你的游戏。
OnResuming 处理 CoreApplication::Resuming。 游戏应用从暂停状态恢复游戏。
OnSuspending 处理 CoreApplication::Suspending。 游戏应用将其状态保存到磁盘。 它有 5 秒时间将状态保存到存储。
OnVisibilityChanged 处理 CoreWindow::VisibilityChanged。 游戏应用的可见性已更改,变为可见或因另一个应用变为可见而致使其不可见。
OnWindowActivationChanged 处理 CoreWindow::Activated。 游戏应用的主窗口已停用或激活,因此它必须删除焦点并暂停游戏,或者重新获得焦点。 在这两种情况下,覆盖层都指示游戏已暂停。
OnWindowClosed 处理 CoreWindow::Closed。 游戏应用关闭主窗口并暂停游戏。
OnWindowSizeChanged 处理 CoreWindow::SizeChanged。 游戏应用重新分配图形资源和覆盖层以适应大小更改,然后更新呈现器目标。

后续步骤

在本主题中,我们了解了如何使用游戏状态管理整个游戏流以及游戏由多个不同的状态机构成。 我们还了解到了如何更新 UI 和管理关键应用事件处理程序。 现在,我们准备深入研究呈现循环、游戏及其构成。

可以按任何顺序浏览介绍此游戏的其余主题。