Marble Maze 应用结构

Applies to Windows only

DirectX Windows 应用商店应用的结构与传统桌面应用的结构不同。Windows 运行时提供了接口(如 Windows::UI::Core::ICoreWindow),以便你可以采用更现代的面向对象的方式开发 Windows 应用商店应用,而不是使用句柄类型(如 HWND)和函数(如 CreateWindow)。本节介绍 Marble Maze 应用代码的结构是如何设计的。

注意  与本文对应的示例代码位于 DirectX Marble Maze 游戏示例中。

本主题内容

本文讨论了在设计游戏代码结构时的一些重要事项:

  • 在初始化阶段,设置游戏使用的运行时和库组件。还要加载特定于游戏的资源。
  • Windows 应用商店应用必须在启动后的 5 秒内开始处理事件。因此,在加载应用时仅加载必要的资源。游戏应在后台加载大型资源并显示一个进度屏幕。
  • 在游戏循环中,响应 Windows 事件,读取用户输入,更新场景对象并渲染场景。
  • 使用事件处理程序响应窗口事件。(这些事件取代了来自桌面 Windows 应用的窗口消息。)
  • 使用状态机来控制游戏逻辑的流向和顺序。

文件整理

Marble Maze 中的一些组件只需很少或无需修改即可在任何游戏中重用。对于你自己的游戏,可以调整这些文件所提供的结构和观点。 下表简短描述了重要的源代码文件。

文件描述
Audio.h、Audio.cpp 定义 Audio 类,它管理音频资源。
BasicLoader.h、BasicLoader.cpp 定义 BasicLoader 类,它提供了实用程序方法来帮助加载纹理、网格和着色器。
BasicMath.h定义帮助你处理顶点和矩阵数据以及计算的结构和函数。其中许多函数与 HLSL 着色器类型兼容。
BasicReaderWriter.h、BasicReaderWriter.cpp 定义 BasicReaderWriter 类,它使用 Windows 运行时在 Windows 应用商店应用中读取和写入文件数据。
BasicShapes.h、BasicShapes.cpp 定义 BasicShapes 类,它提供创建基本形状(如立方体和球)的实用程序方法。(Marble Maze 实现未使用这些文件)。
BasicTimer.h、BasicTimer.cpp 定义 BasicTimer 类,它提供一种轻松的方式来获取总时间和已经历的时间。
Camera.h、Camera.cpp 定义 Camera 类,它提供照相机的位置和方向。
Collision.h、Collision.cpp 管理弹珠与其他物体(例如迷宫)之间的碰撞信息。
DDSTextureLoader.h、DDSTextureLoader.cpp 定义 CreateDDSTextureFromMemory 函数,它从内存缓冲区中加载 .dds 格式的纹理。
DirectXApp.h、DirectXApp.cpp 定义 DirectXAppDirectXAppSource 类,它们包装应用的视图(窗口、线程和事件)。
DirectXBase.h、DirectXBase.cpp 定义 DirectXBase 类,它提供许多 DirectX Windows 应用商店应用通用的基础结构。
DirectXSample.h定义可由 DirectX Windows 应用商店应用使用的实用程序函数。
LoadScreen.h、LoadScreen.cpp 定义 LoadScreen 类,它在应用初始化期间显示一个加载屏幕。
MarbleMaze.h、MarbleMaze.cpp 定义 MarbleMaze 类,它管理特定于游戏的资源并定义许多游戏逻辑。
MediaStreamer.h、MediaStreamer.cpp 定义 MediaStreamer 类,它使用媒体基础帮助游戏管理音频资源。
PersistentState.h、PersistentState.cpp定义 PersistentState 类,它从备份存储读取和向备份存储写入基元数据类型。
Physics.h、Physics.cpp 定义 Physics 类,它实现弹珠与迷宫之间的力学模拟。
Primitives.h 定义游戏使用的几何图形类型。
SampleOverlay.h、SampleOverlay.cpp 定义 SampleOverlay 类,它提供通用的 2D 和用户界面数据与操作。
SDKMesh.h、SDKMesh.cpp 定义 SDKMesh 类,它加载并渲染 SDK Mesh (.sdkmesh) 格式的网格。
UserInterface.h、UserInterface.cpp 定义与用户界面相关的功能,例如菜单系统和高分表。

 

设计时资源格式与运行时资源格式

如果可能,使用运行时格式代替设计时格式,以更高效地加载游戏资源。

设计时格式是在设计资源时使用的格式。通常,3D 设计人员使用设计时格式。一些设计时格式也是基于文本的,以便你可在任何基于文本的编辑器中修改它们。设计时格式可能很繁复,包含比游戏所需更多的信息。运行时格式是游戏读取的二进制格式。运行时格式通常比相应的设计时格式更紧凑,并且能更高效地加载。这正是大多数游戏在运行时使用运行时资产的原因。

尽管游戏可直接读取设计时格式,但使用单独的运行时格式具有许多好处。因为运行时格式常常更加紧凑,所以它们需要的磁盘空间更少,在网络上传输所需的时间也更少。另外,运行时格式常常表示为具有对应内存的数据结构。因此,将其加载到内存中的速度要比(举例而言)基于 XML 的文本文件快得多。最后,因为独立的运行时格式通常是二进制编码的,所以最终用户更难修改它们。

HLSL 着色器是使用不同设计时和运行时格式的一个资源示例。Marble Maze 使用 .hlsl 作为设计时格式,使用 .cso 作为运行时格式。.hlsl 文件包含着色器的源代码;.cso 文件包含相应的着色器字节代码。离线转换 .hlsl 文件并为游戏提供 .cso 文件时,可避免在游戏加载时将 HLSL 源文件转换为字节代码。

出于指令原因,Marble Maze 项目同时包含许多资源的设计时格式和运行时格式,但你在自己游戏的源项目中只需维护设计时格式,因为可在需要时将它们转换为运行时格式。本文介绍如何将设计时格式转换为运行时格式。

应用生命周期

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

Windows 应用商店游戏初始化时,它通常会初始化运行时组件,例如 Direct3D、Direct2D 和它使用的任何输入、音频或力学库。它还在游戏开始之前加载特定于游戏的必要资源。此初始化过程在一个游戏会话中发生一次。

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

添加到模板

DirectX 应用模板将创建一个可使用 Direct3D 渲染到的核心窗口。 模板还包括 DeviceResources 类,该类可创建在 Windows 应用商店应用中渲染 3D 内容所需的所有 Direct3D 设备资源。 AppMain 将在每个帧上创建 MarbleMaze 类对象、开始加载资源、循环以更新计时器,并调用 MarbleMaze 渲染方法。 用于此类的 CreateWindowSizeDependentResources、Update 和 Render 方法调用 MarbleMaze 类中的相应方法。 以下示例显示了 AppMain 构造函数创建 MarbleMaze 类对象的位置。 将设备资源类传递到该类,以便它可以使用 Direct3D 对象进行渲染。


    m_marbleMaze = std::unique_ptr<MarbleMaze>(new MarbleMaze(m_deviceResources));
    m_marbleMaze->CreateWindowSizeDependentResources();


AppMain 类还开始为游戏加载延迟资源。 有关详细信息,请参阅下一部分。 DirectXPage 构造函数将设置事件处理程序、创建 DeviceResources 类,以及创建 AppMain 类。

当这些事件的处理程序被调用时,它们会将输入传递到 MarbleMaze 类。

在后台加载游戏资产

要确保游戏可在启动后的 5 秒内响应窗口事件,我们建议异步或在后台加载游戏资产。资产加载到后台后,游戏即可响应窗口事件。

注意  也可在主菜单准备好后显示主菜单,允许剩余资产继续在后台加载。如果用户在所有资源加载完之前从菜单选择一个选项,你可显示一个进度栏来表明正在继续加载场景资源。

即使游戏包含相对较少的游戏资产,异步加载它们也是一种不错的做法,原因有二。一个原因是很难保证所有资源将快速加载到所有设备和所有配置上。另外,通过尽早整合异步加载功能,代码随时可随着功能的添加而扩展。

异步资产加载从 AppMain::Load 方法开始。此方法使用 task Class (Concurrency Runtime) 类在后台加载游戏资产。



    task<void>([=]()
    {
        m_marbleMaze->LoadDeferredResources();
    });



MarbleMaze 类定义 m_deferredResourcesReady 标志来表明异步加载已完成。MarbleMaze::LoadDeferredResources 方法加载游戏资源,然后设置此标志。应用的更新 (MarbleMaze::Update) 和渲染 (MarbleMaze::Render) 阶段会检查此标志。设置此标志后,游戏会像平常一样继续运行。如果未设置此标志,游戏会显示加载屏幕。

有关 Windows 应用商店应用中异步编程的详细信息,请参阅使用 C++ 的异步编程

提示  如果编写一个 Windows 运行时 C++ 库(也就是 DLL)中包含的游戏代码,请考虑是否查阅使用 C++ 为 Windows 应用商店应用创建异步操作,了解如何创建可供应用和其他库使用的异步操作。

游戏循环

DirectPage::OnRendering 方法运行主要游戏循环。 在每个帧上调用此方法。

为了帮助将视图和窗口代码与特定于游戏的代码分开,我们实现了 DirectXApp::Run 方法来将更新和渲染调用转发到 MarbleMaze 对象。DirectPage::OnRendering 方法还定义了游戏计时器,用于动画和力学模拟。

以下示例展示了 DirectPage::OnRendering 方法,其中包含主要游戏循环。该游戏循环更新总时间和帧时间变量,然后更新和渲染场景。 这样还可确保仅在窗口可见时渲染内容。




void DirectXPage::OnRendering(Object^ sender, Object^ args)
{
    if (m_windowVisible)
    {
        m_main->Update();

        if (m_main->Render())
        {
            m_deviceResources->Present();
        }
    }
}


状态机

游戏通常包含一个状态机(也称为有限状态机或 FSM)来控制游戏逻辑的流和顺序。状态机包含给定数量的状态,并且拥有在它们之间过渡的能力。状态机通常从一个初始状态开始,过渡到一个或多个中间状态,最后可能在一个终止状态上结束。

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


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


举例而言,MainMenu 状态定义主菜单显示和游戏未活动。相反,InGameActive 状态定义游戏是活动的和菜单未显示。 MarbleMaze 类定义 m_gameState 成员变量来保存活动的游戏状态。

MarbleMaze::UpdateMarbleMaze::Render 方法使用 switch 语句执行当前状态的逻辑。下面的示例展示了 MarbleMaze::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 进入电量不足状态时挂起。在用户将游戏移动到前台或 Windows 从电量不足状态中恢复时恢复游戏。一般而言,你无需关闭应用。Windows 可在应用处于挂起状态和 Windows 需要该应用所使用的资源(例如内存)时终止它。Windows 会在应用即将挂起或恢复时通知它,但在应用即将被终止时不会通知它。因此,在 Windows 通知应用它即将挂起时,应用必须能够保存在应用重新启动时还原当前用户状态所需的任何数据。如果应用拥有需要很长时间来保存的重要用户状态,你可能还需要定期保存状态,甚至在应用收到挂起通知之前也要定期保存。Marble Maze 出于两种原因而响应挂起和恢复通知:

  1. 应用挂起时,游戏保存当前游戏状态并暂停音频回放。应用恢复时,游戏恢复音频回放。
  2. 应用关闭并在以后重新启动时,游戏从之前的状态恢复。

Marble Maze 执行以下任务来支持挂起和恢复:

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

为了支持挂起和恢复,Marble Maze 定义了 PersistentState 类。(参见 PersistentState.h 和 PersistentState.cpp)。此类使用 Windows::Foundation::Collections::IPropertySet 接口读取和写入属性。PersistentState 类提供了从备份存储读取和向其写入基元数据类型(例如 boolintfloatXMFLOAT3Platform::String)的方法。


ref class PersistentState
{
public:
    void Initialize(
        _In_ Windows::Foundation::Collections::IPropertySet^ m_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;
};


MarbleMaze 类包含一个 PersistentState 对象。MarbleMaze 构造函数初始化这个对象并提供本地应用数据存储作为备份数据存储。


m_persistentState = ref new PersistentState();
m_persistentState->Initialize(ApplicationData::Current->LocalSettings->Values, "MarbleMaze");

Marble Maze 在弹珠通过一个检查点或目标时(在 MarbleMaze::Update 方法中)和窗口失去焦点时(在 MarbleMaze::OnFocusChange 方法中)保存它的状态。如果游戏包含大量状态数据,我们建议偶尔以一种类似方式将状态保存到持久存储中,因为你只有几秒的时间来响应挂起通知。这样,在应用收到挂起通知时,它只需保存更改的状态数据。

为了响应挂起和恢复通知,DirectXPage 类定义了在挂起和恢复时调用的 SaveInternalStateLoadInternalState 方法。MarbleMaze::OnSuspending 方法处理挂起事件,MarbleMaze::OnResuming 方法处理恢复事件。

MarbleMaze::OnSuspending 方法保存游戏状态并挂起音频。


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


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


void MarbleMaze::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 示例添加音频中介绍。

为了支持重新启动,MarbleMaze::Initialize 方法(在启动期间被调用)调用 MarbleMaze::LoadState 方法。MarbleMaze::LoadState 方法读取状态并将状态应用到游戏对象上。如果游戏在挂起时是暂停或活动的,此方法还会将当前游戏状态设置为暂停。我们暂停游戏是为了让用户不会被意外的活动所惊扰。如果游戏在挂起时未处在玩游戏状态,它还会调出主菜单。


void MarbleMaze::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 不会区分冷启动(也就是首次启动,而之前没有挂起事件)和从挂起状态恢复。这是所有 Windows 应用商店应用的建议设计。

有关演示如何在本地应用数据存储中存储和检索设置和文件的更多示例,请参阅快速入门:本地应用程序数据。有关应用数据的详细信息,请参阅使用 Windows 运行时访问应用数据

后续步骤

查阅向 Marble Maze 示例添加可视内容,了解在使用可视资源时要记住的一些重要实践。

相关主题

向 Marble Maze 示例添加可视内容
Marble Maze 示例基础
开发 Marble Maze,一款使用 C++ 和 DirectX 的 Windows 应用商店游戏

 

 

显示:
© 2014 Microsoft