Marble Maze 应用程序结构

DirectX 通用 Windows 平台 (UWP) 应用的结构不同于传统桌面应用程序的结构。 Windows 运行时提供如 Windows::UI::Core::ICoreWindow 的接口,而不是使用句柄类型如 HWND 和函数 CreateWindow,以便你可以以更现代、面向对象的方式开发 UWP 应用。 本文档的此部分介绍了 Marble Maze 应用代码的结构。

注释

DirectX Marble Maze 游戏示例中找到与此文档对应的示例代码。

以下是本文档在构建游戏代码时讨论的一些要点:

  • 在初始化阶段,设置游戏使用的运行时和库组件,并加载特定于游戏的资源。
  • UWP 应用必须在启动后的 5 秒内开始处理事件。 因此,加载应用时仅加载基本资源。 游戏应在后台加载大型资源并显示进度屏幕。
  • 在游戏循环中,响应 Windows 事件、读取用户输入、更新场景对象以及呈现场景。
  • 使用事件处理程序响应窗口事件。 (这些代码将替换桌面 Windows 应用程序中的窗口消息。
  • 使用状态机控制游戏逻辑的流和顺序。

文件组织

在 Marble Maze 中,有一些组件可以在任何游戏中重复使用,几乎不需要修改。 对于你自己的游戏,你可以改编这些文件中提供的组织和想法。 下表简要介绍了重要的源代码文件。

文件 DESCRIPTION
App.h、App.cpp 定义 应用DirectXApplicationSource 类,这些类封装应用的视图(窗口、线程和事件)。
Audio.h、Audio.cpp 定义用于管理音频资源的 Audio 类。
BasicLoader.h,BasicLoader.cpp 定义 BasicLoader 类,该类提供有助于加载纹理、网格和着色器的实用工具方法。
BasicMath.h 定义有助于处理向量和矩阵数据和计算的结构和函数。 其中许多函数都与 HLSL 着色器类型兼容。
BasicReaderWriter.h,BasicReaderWriter.cpp 定义 BasicReaderWriter 类,该类使用 Windows 运行时在 UWP 应用中读取和写入文件数据。
BasicShapes.h、BasicShapes.cpp 定义 BasicShapes 类,该类提供用于创建基本形状(如立方体和球体)的实用工具方法。 (Marble Maze 实现不使用这些文件)。
Camera.h,Camera.cpp 定义 Camera 类,该类提供相机的位置和方向。
Collision.h、Collision.cpp 管理弹珠和其他对象(如迷宫)之间的碰撞信息。
DDSTextureLoader.h,DDSTextureLoader.cpp 定义 CreateDDSTextureFromMemory 函数,该函数从内存缓冲区加载.dds格式的纹理。
DirectXHelper.h 定义对许多 DirectX UWP 应用有用的 DirectX 帮助程序函数。
LoadScreen.h,LoadScreen.cpp 定义 LoadScreen 类,该类在应用初始化期间显示加载屏幕。
MarbleMazeMain.h,MarbleMazeMain.cpp 定义 MarbleMazeMain 类,该类管理特定于游戏的资源并定义大部分游戏逻辑。
MediaStreamer.h,MediaStreamer.cpp 定义 MediaStreamer 类,该类使用 Media Foundation 帮助游戏管理音频资源。
PersistentState.h,PersistentState.cpp 定义 PersistentState 类,该类从/向后存储读取和写入基元数据类型。
Physics.h,Physics.cpp 定义 物理学 类,该类用于实现弹珠和迷宫之间的物理模拟。
Primitives.h 定义游戏使用的几何类型。
SampleOverlay.h,SampleOverlay.cpp 定义 SampleOverlay 类,该类提供常见的 2D 和用户界面数据和操作。
SDKMesh.h,SDKMesh.cpp 定义 SDKMesh 类,该类加载和呈现采用 SDK 网格(.sdkmesh)格式的网格。
StepTimer.h 定义 StepTimer 类,该类提供了获取总时间和已用时间的简单方法。
UserInterface.h、UserInterface.cpp 定义与用户界面相关的功能,例如菜单系统和高分表。

 

设计时与运行时资源格式

可以时,使用运行时格式而不是设计时格式来更有效地加载游戏资源。

设计时的 格式是您设计资源时使用的格式。 通常,3D 设计器使用设计时格式。 某些设计时格式也是基于文本的格式,因此你可以在任何基于文本的编辑器中修改它们。 在设计阶段的格式可能比较冗长,并且包含的信息比游戏所需的更多。 运行时 格式是被游戏读取的二进制格式。 运行时格式通常比相应的设计时格式更紧凑、加载起来更高效。 这就是为什么大多数游戏在运行时使用运行时资产的原因。

虽然游戏可以直接读取设计时格式,但使用单独的运行时格式有几个好处。 由于运行时格式通常更紧凑,因此需要更少的磁盘空间,并且需要更少的时间来通过网络传输。 此外,运行时格式通常表示为内存映射的数据结构。 因此,它们被加载到内存的速度要比,例如,基于 XML 的文本文件快得多。 最后,由于单独的运行时格式通常是二进制编码的,因此最终用户更难修改它们。

HLSL 着色器是使用不同设计时和运行时格式的资源的一个示例。 Marble Maze 使用 .hlsl 作为设计时格式,将 .cso 用作运行时格式。 .hlsl 文件保存着色器的源代码;.cso 文件保存相应的着色器字节代码。 当你脱机转换 .hlsl 文件并为游戏提供 .cso 文件时,你无需在游戏加载时将 HLSL 源文件转换为字节代码。

为了说明目的,Marble Maze 项目包括许多资源的设计时格式和运行时格式,但在你自己的游戏的源项目中,你只需要维护设计时格式,因为你可以在需要时将其转换为运行时格式。 本文档演示如何将设计时格式转换为运行时格式。

应用程序生命周期

Marble Maze 遵循典型 UWP 应用的生命周期。 有关 UWP 应用的生命周期的详细信息,请参阅 应用生命周期

当 UWP 游戏初始化时,它通常会初始化运行时组件,例如 Direct3D、Direct2D 以及它使用的任何输入、音频或物理库。 它还会加载在游戏开始前所需的游戏特定的资源。 此初始化在游戏会话期间发生一次。

初始化后,游戏通常运行 游戏循环。 在此循环中,游戏通常执行四个操作:处理 Windows 事件、收集输入、更新场景对象和呈现场景。 当游戏更新场景时,它可以将当前输入状态应用于场景对象并模拟物理事件,例如对象冲突。 游戏还可以执行其他活动,例如播放声音效果或通过网络发送数据。 当游戏呈现场景时,它会捕获场景的当前状态,并将其绘制到显示设备。 以下部分更详细地介绍了这些活动。

添加到模板

DirectX 11 应用(通用 Windows) 模板创建一个核心窗口,可以使用 Direct3D 呈现该窗口。 该模板还包括 DeviceResources 类,该类创建在 UWP 应用中呈现 3D 内容所需的所有 Direct3D 设备资源。

App 类创建 MarbleMazeMain 类对象,启动资源加载,循环更新计时器,并在每帧调用 MarbleMazeMain::Render 方法。 App::OnWindowSizeChangedApp::OnDpiChangedApp::OnOrientationChanged 方法分别调用 MarbleMazeMain::CreateWindowSizeDependentResources 方法, 和 App::Run 方法调用 MarbleMazeMain::UpdateMarbleMazeMain::Render 方法。

以下示例显示了 App::SetWindow 方法创建 MarbleMazeMain 类对象的位置。 DeviceResources 类传递给该方法,以便它可以使用 Direct3D 对象进行呈现。

    m_main = std::unique_ptr<MarbleMazeMain>(new MarbleMazeMain(m_deviceResources));

应用程序的 类还会开始加载游戏的延迟资源。 有关更多详细信息,请参阅下一部分。

此外,App 类为 CoreWindow 事件设置事件处理程序。 调用这些事件的处理程序时,会将输入传递给 MarbleMazeMain 类。

在后台加载游戏资源

为了确保游戏可以在启动后 5 秒内响应窗口事件,建议异步或在后台加载游戏资产。 当资产在后台加载时,游戏可以响应窗口事件。

注释

还可以在主菜单准备就绪后显示主菜单,并允许剩余资产继续在后台加载。 例如,如果用户在加载所有资源之前从菜单中选择一个选项,则可以通过显示进度栏来指示场景资源继续加载。

 

即使你的游戏包含相对较少的游戏资产,最好以两个原因异步加载它们。 一个原因是很难保证所有资源都会在所有设备和所有配置上快速加载。 此外,通过提前采用异步加载,代码可在增加功能时准备好扩展。

异步资产加载从 App::Load 方法开始。 此方法使用 任务 类在后台加载游戏资产。

    task<void>([=]()
    {
        m_main->LoadDeferredResources(true, false);
    });

MarbleMazeMain 类定义 m_deferredResourcesReady 标志,以指示异步加载已完成。 MarbleMazeMain::LoadDeferredResources 方法加载游戏资源,然后设置此标志。 更新(MarbleMazeMain::Update)和渲染(MarbleMazeMain::Render)阶段的应用程序会检查此标志。 设置此标志后,游戏会像正常一样继续。 如果标记还没有设置,游戏将显示加载屏幕。

有关 UWP 应用的异步编程的详细信息,请参阅 C++中的 异步编程。

小窍门

如果您正在编写属于 Windows 运行时 C++ 库(也就是说,DLL)的游戏代码,请考虑阅读 如何在 C++ 中为 UWP 应用创建异步操作,以了解如何创建可供应用和其他库使用的异步操作。

 

游戏循环

App::Run 方法会运行主要游戏循环(MarbleMazeMain::Update)。 此方法称为每个帧。

为了帮助将视图和窗口代码与特定于游戏的代码区分开,我们实现了 App::Run 方法,以便将更新和呈现的调用转发到 MarbleMazeMain 对象。

以下示例显示了 App::Run 方法,其中包括主游戏循环。 游戏循环更新总时间和帧时间变量,然后更新并呈现场景。 这也可确保仅当窗口可见时才会呈现内容。

void App::Run()
{
    while (!m_windowClosed)
    {
        if (m_windowVisible)
        {
            CoreWindow::GetForCurrentThread()->Dispatcher->
                ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_main->Update();

            if (m_main->Render())
            {
                m_deviceResources->Present();
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread()->Dispatcher->
                ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }

    // The app is exiting so do the same thing as if the app were being suspended.
    m_main->OnSuspending();

#ifdef _DEBUG
    // Dump debug info when exiting.
    DumpD3DDebug();
#endif //_DEGBUG
}

状态机

游戏通常包含 状态机(也称为 有限状态机或 FSM)来控制游戏逻辑的流和顺序。 状态机包含给定数量的状态,并且能够在其中转换。 状态机通常从初始 状态 开始,过渡到一个或多个 中间 状态,并且可能以 终端 状态结束。

游戏循环通常使用状态机,以便它可以执行特定于当前游戏状态的逻辑。 Marble Maze 定义了 GameState 枚举,该枚举定义了游戏的每个可能状态。

enum class GameState
{
    Initial,
    MainMenu,
    HighScoreDisplay,
    PreGameCountdown,
    InGameActive,
    InGamePaused,
    PostGameResults,
};

例如,MainMenu 状态定义了主菜单的显示,即游戏未处于活动状态。 相反,InGameActive 状态定义游戏处于活动状态,并且菜单未显示。 MarbleMazeMain 类定义用于保存活动游戏状态的 m_gameState 成员变量。

MarbleMazeMain::UpdateMarbleMazeMain::Render 方法使用 switch 语句来执行当前状态的逻辑。 以下示例展示了 MarbleMazeMain::Update 方法(为说明结构而省略详细信息)中 switch 语句可能的样子。

switch (m_gameState)
{
case GameState::MainMenu:
    // Do something with the main menu. 
    break;

case GameState::HighScoreDisplay:
    // Do something with the high-score table. 
    break;

case GameState::PostGameResults:
    // Do something with the game results. 
    break;

case GameState::InGamePaused:
    // Handle the paused state. 
    break;
}

当游戏逻辑或呈现依赖于特定游戏状态时,我们将在本文档中强调它。

处理应用和窗口事件

Windows 运行时提供面向对象的事件处理系统,以便更轻松地管理 Windows 消息。 若要在应用程序中使用事件,必须提供响应事件的事件处理程序或事件处理方法。 还必须向事件源注册事件处理程序。 此过程通常称为事件绑定。

支持暂停、恢复和重启

当用户离开 Marble Maze 或 Windows 进入低功率状态时,Marble Maze 将暂停。 当用户将游戏移动到前台或当 Windows 退出低电源状态时,游戏将恢复。 通常,你不会关闭应用。 当应用处于挂起状态并且 Windows 需要应用所使用的资源(如内存)时,Windows 可以终止应用。 Windows 会在应用即将暂停或恢复时通知应用,但在即将终止时不会通知应用。 因此,你的应用必须能够保存(当 Windows 通知你的应用即将暂停时)在重启应用时还原当前用户状态所需的任何数据。 如果应用的用户数据状态保存起来开销很大,那么甚至在应用收到暂停通知之前,也可能需要定期保存状态。 Marble Maze 对暂停和恢复通知作出响应,原因有两个:

  1. 当应用暂停时,游戏将保存当前游戏状态并暂停音频播放。 恢复应用后,游戏将恢复音频播放。
  2. 当应用关闭并稍后重新启动时,游戏将从其以前的状态恢复。

Marble Maze 执行以下任务以支持暂停和恢复:

  • 它会在游戏中关键点(例如用户到达检查点时)将状态保存到持久存储中。
  • 它会通过将通知的状态保存到持久性存储来响应暂停通知。
  • 它通过从永久性存储加载其状态来响应恢复通知。 它还会在启动期间加载以前的状态。

为了支持暂停和恢复,Marble Maze 定义了 PersistentState 类。 (请参阅 PersistentState.hPersistentState.cpp)。 此类使用 Windows::Foundation::Collections::IPropertySet 接口来读取和写入属性。 PersistentState 类提供读取和写入基元数据类型的方法(例如 boolintfloatXMFLOAT3Platform::String),从和到后备存储。

ref class PersistentState
{
internal:
    void Initialize(
        _In_ Windows::Foundation::Collections::IPropertySet^ settingsValues,
        _In_ Platform::String^ key
        );

    void SaveBool(Platform::String^ key, bool value);
    void SaveInt32(Platform::String^ key, int value);
    void SaveSingle(Platform::String^ key, float value);
    void SaveXMFLOAT3(Platform::String^ key, DirectX::XMFLOAT3 value);
    void SaveString(Platform::String^ key, Platform::String^ string);

    bool LoadBool(Platform::String^ key, bool defaultValue);
    int  LoadInt32(Platform::String^ key, int defaultValue);
    float LoadSingle(Platform::String^ key, float defaultValue);

    DirectX::XMFLOAT3 LoadXMFLOAT3(
        Platform::String^ key, 
        DirectX::XMFLOAT3 defaultValue);

    Platform::String^ LoadString(
        Platform::String^ key, 
        Platform::String^ defaultValue);

private:
    Platform::String^ m_keyName;
    Windows::Foundation::Collections::IPropertySet^ m_settingsValues;
};

MarbleMazeMain 类保存 PersistentState 对象。 MarbleMazeMain 构造函数初始化此对象,并提供本地应用程序数据存储作为后盾数据存储。

m_persistentState = ref new PersistentState();

m_persistentState->Initialize(
    Windows::Storage::ApplicationData::Current->LocalSettings->Values,
    "MarbleMaze");

Marble Maze 在弹珠越过检查点或到达目标时保存状态(在 MarbleMazeMain::Update 方法中),并且当窗口失去焦点时(在 MarbleMazeMain::OnFocusChange 方法中)。 如果你的游戏保存了大量状态数据,我们建议你偶尔以类似的方式将状态保存到永久性存储,因为你只需几秒钟来响应暂停通知。 因此,当应用收到暂停通知时,它只需保存已更改的状态数据。

为了响应暂停和恢复通知,MarbleMazeMain 类定义 SaveStateLoadState 方法,这些方法在暂停和恢复时调用。 MarbleMazeMain::OnSuspending 方法处理挂起事件,MarbleMazeMain::OnResuming 方法处理恢复事件。

MarbleMazeMain::OnSuspending 方法保存游戏状态并暂停音频。

void MarbleMazeMain::OnSuspending()
{
    SaveState();
    m_audio.SuspendAudio();
}

MarbleMazeMain::SaveState 方法保存游戏状态值,例如弹珠的当前位置和速度、最近的检查点和高分表。

void MarbleMazeMain::SaveState()
{
    m_persistentState->SaveXMFLOAT3(":Position", m_physics.GetPosition());
    m_persistentState->SaveXMFLOAT3(":Velocity", m_physics.GetVelocity());

    m_persistentState->SaveSingle(
        ":ElapsedTime", 
        m_inGameStopwatchTimer.GetElapsedTime());

    m_persistentState->SaveInt32(":GameState", static_cast<int>(m_gameState));
    m_persistentState->SaveInt32(":Checkpoint", static_cast<int>(m_currentCheckpoint));

    int i = 0;
    HighScoreEntries entries = m_highScoreTable.GetEntries();
    const int bufferLength = 16;
    char16 str[bufferLength];

    m_persistentState->SaveInt32(":ScoreCount", static_cast<int>(entries.size()));

    for (auto iter = entries.begin(); iter != entries.end(); ++iter)
    {
        int len = swprintf_s(str, bufferLength, L"%d", i++);
        Platform::String^ string = ref new Platform::String(str, len);

        m_persistentState->SaveSingle(
            Platform::String::Concat(":ScoreTime", string), 
            iter->elapsedTime);

        m_persistentState->SaveString(
            Platform::String::Concat(":ScoreTag", string), 
            iter->tag);
    }
}

游戏恢复时,它只需要恢复音频。 它不必从永久性存储加载状态,因为状态已在内存中加载。

将音频添加到 Marble Maze 示例文档中介绍了游戏暂停和恢复音频的方式。

为了支持重启,MarbleMazeMain 构造函数(在启动时调用)调用 MarbleMazeMain::LoadState 方法。 MarbleMazeMain::LoadState 方法读取并将状态应用于游戏对象。 此方法还会将当前游戏状态设置为暂停(如果游戏在暂停时已暂停或处于活动状态)。 我们暂停游戏,以便用户对意外的活动并不感到惊讶。 如果游戏在暂停时未处于游戏状态,它还会移动到主菜单。

void MarbleMazeMain::LoadState()
{
    XMFLOAT3 position = m_persistentState->LoadXMFLOAT3(
        ":Position", 
        m_physics.GetPosition());

    XMFLOAT3 velocity = m_persistentState->LoadXMFLOAT3(
        ":Velocity", 
        m_physics.GetVelocity());

    float elapsedTime = m_persistentState->LoadSingle(":ElapsedTime", 0.0f);

    int gameState = m_persistentState->LoadInt32(
        ":GameState", 
        static_cast<int>(m_gameState));

    int currentCheckpoint = m_persistentState->LoadInt32(
        ":Checkpoint", 
        static_cast<int>(m_currentCheckpoint));

    switch (static_cast<GameState>(gameState))
    {
    case GameState::Initial:
        break;

    case GameState::MainMenu:
    case GameState::HighScoreDisplay:
    case GameState::PreGameCountdown:
    case GameState::PostGameResults:
        SetGameState(GameState::MainMenu);
        break;

    case GameState::InGameActive:
    case GameState::InGamePaused:
        m_inGameStopwatchTimer.SetVisible(true);
        m_inGameStopwatchTimer.SetElapsedTime(elapsedTime);
        m_physics.SetPosition(position);
        m_physics.SetVelocity(velocity);
        m_currentCheckpoint = currentCheckpoint;
        SetGameState(GameState::InGamePaused);
        break;
    }

    int count = m_persistentState->LoadInt32(":ScoreCount", 0);

    const int bufferLength = 16;
    char16 str[bufferLength];

    for (int i = 0; i < count; i++)
    {
        HighScoreEntry entry;
        int len = swprintf_s(str, bufferLength, L"%d", i);
        Platform::String^ string = ref new Platform::String(str, len);

        entry.elapsedTime = m_persistentState->LoadSingle(
            Platform::String::Concat(":ScoreTime", string), 
            0.0f);

        entry.tag = m_persistentState->LoadString(
            Platform::String::Concat(":ScoreTag", string), 
            L"");

        m_highScoreTable.AddScoreToTable(entry);
    }
}

重要

Marble Maze 不区分冷启动(即在未发生以前的暂停事件的情况下第一次开始)以及从暂停状态恢复。 建议对所有 UWP 应用进行此设计。

有关应用程序数据的详细信息,请参阅 存储和检索设置和其他应用数据

后续步骤

阅读 将视觉内容添加到 Marble Maze 示例,了解在使用视觉资源时要记住的一些关键做法。