游戏流管理

注释

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

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

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

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

Simple3DGameDX 示例游戏首次在计算机上运行时,它处于未开始任何游戏的状态。 游戏的后续运行时间,它可以处于上述任一状态。

  • 游戏尚未开始,或者游戏正在关卡之间(高分为零)。
  • 游戏循环正在运行,并且正在关卡的中间。
  • 由于游戏已完成(高分具有非零值),游戏循环未运行。

你的游戏可以根据需要拥有任意数量的状态。 但请记住,它可以随时终止。 当它恢复时,用户期望它在终止时的状态中恢复。

游戏状态管理

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

为了响应暂停事件,游戏已暂停,但游戏的资源仍在内存中。 同样,处理恢复事件,以确保示例游戏能够从暂停或被关闭时的状态继续。 根据状态,向玩家显示不同的选项。

  • 如果游戏在中途恢复,则它似乎暂停,叠加界面提供继续的选项。
  • 如果游戏在完成后恢复,则会显示高分,并有一个选项来玩新游戏。
  • 最后,如果在新的级别启动之前游戏恢复,界面会向用户提供一个开始选项。

示例游戏不区分游戏是冷启动、首次启动而不发生暂停事件,还是从暂停状态恢复。 这是任何 UWP 应用的正确设计。

在此示例中,游戏状态的初始化发生在 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。 玩家完成所有级别。

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

在规划游戏时,请考虑绘制整个游戏流,以确保你已解决用户或系统可以采取的所有可能操作。 游戏可能非常复杂,因此状态机是一种功能强大的工具,可帮助你直观显示这种复杂性,并使其更易于管理。

让我们看看更新循环的代码。

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。
  • 暂停位图,它是一个黑色矩形,在游戏的暂停/暂停状态期间覆盖文本。 这是游戏界面覆盖层。 我们将在 添加用户界面中进一步讨论。

毫不奇怪,覆盖层也有状态机。 覆盖可以显示关卡开始或游戏结束的消息。 它本质上是一个画布,在游戏暂停或挂起时,我们可以在其上输出任何有关游戏状态的信息以显示给玩家。

渲染的覆盖界面可以是以下六个屏幕之一,具体取决于游戏的状态。

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

正确处理相关事件对于 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 和管理关键应用事件处理程序。 现在,我们已准备好深入了解呈现循环、游戏及其机制。

你可以按任意顺序浏览记录此游戏的剩余主题。